1 Commits

Author SHA1 Message Date
philipp
5ee776317b Update Cargo dependencies
All checks were successful
CI/CD Pipeline / test (push) Successful in 30m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-06-13 12:38:42 +00:00
18 changed files with 473 additions and 597 deletions

View File

@@ -17,9 +17,6 @@ jobs:
- name: Run Test DB Script - name: Run Test DB Script
run: ./test_db.sh run: ./test_db.sh
- name: Test
run: npm --version
- name: Cache Cargo dependencies - name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@@ -28,15 +25,15 @@ jobs:
cargo build cargo build
cd frontend && npm install && npm run build cd frontend && npm install && npm run build
- name: Frontend tests - name: Frontend tests
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter html,line run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
- name: Backend tests - name: Backend tests
run: cargo test --verbose run: cargo test --verbose
#- uses: actions/upload-artifact@v3
# if: always()
# with:
# name: playwright-report
# path: frontend/playwright-report/
# retention-days: 30
deploy-staging: deploy-staging:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -66,16 +63,16 @@ jobs:
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/root/rowing-staging/rot-updating scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -C -r static $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -C -r templates $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -C -r svelte $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rowing-staging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
ssh $SSH_USER@$SSH_HOST 'rm -f /root/rowing-staging/db.sqlite && cp /root/rowing-prod/db.sqlite /root/rowing-staging/db.sqlite && mkdir -p /root/rowing-staging/svelte/build && mkdir -p /root/rowing-staging/data-ergo/thirty && mkdir -p /root/rowing-staging/data-ergo/dozen && sqlite3 /root/rowing-staging/db.sqlite < /root/rowing-staging/staging-diff.sql' ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql'
ssh $SSH_USER@$SSH_HOST 'mv /root/rowing-staging/rot-updating /root/rowing-staging/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rowing-staging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }} SSH_HOST: ${{ secrets.SSH_HOST }}
@@ -109,14 +106,14 @@ jobs:
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/root/rowing-prod/rot-updating scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
scp -C -r static $SSH_USER@$SSH_HOST:/root/rowing-prod/ scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing/
scp -C -r templates $SSH_USER@$SSH_HOST:/root/rowing-prod/ scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing/
scp -C -r svelte $SSH_USER@$SSH_HOST:/root/rowing-prod/ scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /root/rowing-prod/svelte/build && mkdir -p /root/rowing-prod/data-ergo/thirty && mkdir -p /root/rowing-prod/data-ergo/dozen' ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/rowing/svelte/build && mkdir -p /home/rowing/data-ergo/thirty && mkdir -p /home/rowing/data-ergo/dozen'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rowing-prod' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
ssh $SSH_USER@$SSH_HOST 'mv /root/rowing-prod/rot-updating /root/rowing-prod/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/rowing/rot-updating /home/rowing/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rowing-prod' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }} SSH_HOST: ${{ secrets.SSH_HOST }}

499
Cargo.lock generated

File diff suppressed because it is too large Load Diff

2
fd
View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
scp root@app.rudernlinz.at:/root/rowing-prod/db.sqlite db.sqlite scp root@128.140.64.118:/home/rowing/db.sqlite db.sqlite
#sqlite3 db.sqlite < seeds.sql #sqlite3 db.sqlite < seeds.sql

View File

@@ -413,7 +413,6 @@ function initNewChoice(select: HTMLInputElement) {
steering_person.setAttribute("required", "required"); steering_person.setAttribute("required", "required");
} }
const choice = new Choices(select, { const choice = new Choices(select, {
searchResultLimit: 100,
searchFields: ["label", "value", "customProperties.searchableText"], searchFields: ["label", "value", "customProperties.searchableText"],
removeItemButton: true, removeItemButton: true,
loadingText: "Wird geladen...", loadingText: "Wird geladen...",

View File

@@ -16,7 +16,7 @@
"postcss": "^8.4.21", "postcss": "^8.4.21",
"sass": "^1.60.0", "sass": "^1.60.0",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",
"typescript": "^5.9.3", "typescript": "^4.9.5",
"vite": "^4.2.0", "vite": "^4.2.0",
"vite-plugin-static-copy": "^0.13.1" "vite-plugin-static-copy": "^0.13.1"
}, },

6
package-lock.json generated
View File

@@ -1,6 +0,0 @@
{
"name": "rowt",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -1,8 +1,8 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm; use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use crate::model::boathouse::Boathouse; use crate::model::boathouse::Boathouse;

View File

@@ -1,10 +1,7 @@
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use crate::{ use crate::tera::board::boathouse::FormBoathouseToAdd;
model::{log::Log, user::AllowedToUpdateBoathouse},
tera::board::boathouse::FormBoathouseToAdd,
};
use super::boat::Boat; use super::boat::Boat;
@@ -117,11 +114,7 @@ impl Boathouse {
BoathouseAisles::from(db, boathouses).await BoathouseAisles::from(db, boathouses).await
} }
pub async fn create( pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {
db: &SqlitePool,
changed_by: &AllowedToUpdateBoathouse,
data: FormBoathouseToAdd,
) -> Result<(), String> {
sqlx::query!( sqlx::query!(
"INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)", "INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)",
data.boat_id, data.boat_id,
@@ -132,17 +125,6 @@ impl Boathouse {
.execute(db) .execute(db)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let boat = Boat::find_by_id(db, data.boat_id).await.unwrap();
Log::create(
db,
format!(
"{changed_by} hat das Boot {boat} auf den Gang {}, Seite {}, und Höhe {} 'gelegt'.",
data.aisle, data.side, data.level
),
)
.await;
Ok(()) Ok(())
} }
@@ -153,20 +135,10 @@ impl Boathouse {
.ok() .ok()
} }
pub async fn delete(&self, db: &SqlitePool, changed_by: &AllowedToUpdateBoathouse) { pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id) sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id)
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a Boat of a valid id .unwrap(); //Okay, because we can only create a Boat of a valid id
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
Log::create(
db,
format!(
"{changed_by} hat das Boot {boat} von Gang {}, Seite {}, und Höhe {} gelöscht.",
self.aisle, self.side, self.level
),
)
.await;
} }
} }

View File

@@ -8,7 +8,7 @@ use crate::model::{
notification::Notification, notification::Notification,
role::Role, role::Role,
}; };
use chrono::{Datelike, Local, NaiveDate}; use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -342,33 +342,12 @@ impl User {
None, None,
) )
.await; .await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitglied gemacht (keine Steuerperson/Bootsführer mehr)")) ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitglied gemacht (keine Steuerperson/Schiffsführer mehr)"))
.user(self) .user(self)
.save(db) .save(db)
.await; .await;
} }
} }
(old, new) if old == Some(bootsfuehrer.clone()) && new == Some(cox.clone()) => {
self.remove_role(db, updated_by, &bootsfuehrer).await?;
self.add_role(db, updated_by, &cox).await?;
Notification::create_for_role(
db,
&member,
&format!(
"Lieber Vorstand, {self} ist ab sofort kein Bootsführer:in mehr, sondern 'nur' mehr eine Steuerperson."
),
"Bootsführer--",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{updated_by} hat {self} zur Steuerperson gemacht (kein Bootsführer mehr)"
))
.user(self)
.save(db)
.await;
}
(old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")), (old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")),
}; };
@@ -529,13 +508,6 @@ impl User {
} }
pub(crate) async fn remove_membership_pdf(&self, db: &SqlitePool, updated_by: &ManageUserUser) { pub(crate) async fn remove_membership_pdf(&self, db: &SqlitePool, updated_by: &ManageUserUser) {
ActivityBuilder::new(&format!(
"{updated_by} hat die Beitrittserklärung vom Beutzer gelöscht."
))
.user(self)
.save(db)
.await;
sqlx::query!( sqlx::query!(
"UPDATE user SET membership_pdf = null where id = ?", "UPDATE user SET membership_pdf = null where id = ?",
self.id self.id
@@ -578,32 +550,4 @@ impl User {
Ok(()) Ok(())
} }
pub(crate) async fn has_to_pay_einschreibgebuehr_this_year(&self, db: &SqlitePool) -> bool {
if !self.has_role(db, "schnupperant").await {
if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) =
NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await
{
return true;
}
}
}
}
false
}
pub(crate) fn has_to_pay_only_half(&self) -> bool {
if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
let halfprice_startdate =
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
return member_since_date >= halfprice_startdate;
}
}
false
}
} }

View File

@@ -1,9 +1,10 @@
use super::User; use super::User;
use crate::{ use crate::{
model::family::Family, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND,
FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED,
TRIAL_ROWING_REDUCED, UNTERSTUETZEND, UNTERSTUETZEND, model::family::Family,
}; };
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -80,52 +81,30 @@ impl User {
let mut fee = Fee::new(); let mut fee = Fee::new();
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await { if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
let mut einschreibgebuehr = false;
let mut half_price = true;
for member in family.members(db).await { for member in family.members(db).await {
fee.add_person(&member); fee.add_person(&member);
if member.has_role(db, "paid").await { if member.has_role(db, "paid").await {
fee.paid(); fee.paid();
} }
fee.merge(member.fee_without_families(db, true).await); fee.merge(member.fee_without_families(db).await);
if member.has_to_pay_einschreibgebuehr_this_year(db).await {
einschreibgebuehr = true;
}
if !member.has_to_pay_only_half() {
half_price = false;
}
} }
if family.amount_family_members(db).await > 2 { if family.amount_family_members(db).await > 2 {
if half_price {
fee.add(
"Familie 3+ Personen (Halbpreis)".into(),
FAMILY_THREE_OR_MORE / 2,
);
} else {
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE); fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
}
} else {
if half_price {
fee.add("Familie 2 Personen (Halbpreis)".into(), FAMILY_TWO / 2);
} else { } else {
fee.add("Familie 2 Personen".into(), FAMILY_TWO); fee.add("Familie 2 Personen".into(), FAMILY_TWO);
} }
}
if einschreibgebuehr {
fee.add("Einschreibgebühr (Familie)".into(), EINSCHREIBGEBUEHR);
}
} else { } else {
fee.add_person(self); fee.add_person(self);
if self.has_role(db, "paid").await { if self.has_role(db, "paid").await {
fee.paid(); fee.paid();
} }
fee.merge(self.fee_without_families(db, false).await); fee.merge(self.fee_without_families(db).await);
} }
Some(fee) Some(fee)
} }
async fn fee_without_families(&self, db: &SqlitePool, entry_fee_paid_with_family: bool) -> Fee { async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
let mut fee = Fee::new(); let mut fee = Fee::new();
if !self.has_role(db, "Donau Linz").await if !self.has_role(db, "Donau Linz").await
@@ -146,24 +125,38 @@ impl User {
let amount_boats = self.amount_boats(db).await; let amount_boats = self.amount_boats(db).await;
if amount_boats > 0 { if amount_boats > 0 {
if self.has_to_pay_only_half() {
fee.add(
format!("{}x Bootsplatz (Halbpreis)", amount_boats),
amount_boats * BOAT_STORAGE / 2,
);
} else {
fee.add( fee.add(
format!("{}x Bootsplatz", amount_boats), format!("{}x Bootsplatz", amount_boats),
amount_boats * BOAT_STORAGE, amount_boats * BOAT_STORAGE,
); );
} }
}
if self.has_to_pay_einschreibgebuehr_this_year(db).await && !entry_fee_paid_with_family { if !self.has_role(db, "schnupperant").await {
if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) =
NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await
{
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR); fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
} }
}
}
}
let halfprice = self.has_to_pay_only_half(); let halfprice = if let Some(member_since_date) = &self.member_since_date {
match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") {
Ok(member_since_date) => {
let halfprice_startdate =
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
member_since_date >= halfprice_startdate
}
Err(_) => false,
}
} else {
false
};
if self.has_role(db, "schnupperant").await { if self.has_role(db, "schnupperant").await {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await { if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {

View File

@@ -102,13 +102,6 @@ impl UserWithDetails {
user, user,
} }
} }
pub fn allowed_to_row(&self) -> bool {
self.roles.contains(&"Donau Linz".into())
|| self.roles.contains(&"Förderndes Mitglied".into())
|| self.roles.contains(&"scheckbuch".into())
|| self.user.name == "Externe Steuerperson"
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -431,7 +424,7 @@ WHERE family_id IS NULL;
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token
FROM user FROM user
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id in (SELECT id FROM role WHERE name = 'cox' or name = 'Bootsführer')) > 0 WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
ORDER BY last_access DESC ORDER BY last_access DESC
" "
) )
@@ -795,7 +788,6 @@ macro_rules! special_user {
} }
impl $name { impl $name {
#[allow(dead_code)]
pub fn into_inner(self) -> User { pub fn into_inner(self) -> User {
self.user self.user
} }
@@ -857,10 +849,9 @@ special_user!(ErgoUser, +"ergo");
special_user!(SteeringUser, +"cox", +"Bootsführer"); special_user!(SteeringUser, +"cox", +"Bootsführer");
special_user!(AdminUser, +"admin"); special_user!(AdminUser, +"admin");
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied"); special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied");
special_user!(DonauLinzUser, +"Donau Linz", +"Förderndes Mitglied", -"Unterstützend"); // TODO: special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied"); // TODO:
// remove -> // remove ->
// RegularUser // RegularUser
special_user!(ErgoAdminUser, +"ergo-admin", +"admin");
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer"); special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
special_user!(VorstandUser, +"admin", +"Vorstand"); special_user!(VorstandUser, +"admin", +"Vorstand");
special_user!(EventUser, +"manage_events"); special_user!(EventUser, +"manage_events");
@@ -868,7 +859,6 @@ special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
special_user!(ManageUserUser, +"admin", +"schriftfuehrer"); special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
special_user!(AllowedToSendFeeReminderUser, +"admin", +"schriftfuehrer", +"kassier"); special_user!(AllowedToSendFeeReminderUser, +"admin", +"schriftfuehrer", +"kassier");
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin"); special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
special_user!(AllowedToUpdateBoathouse, +"admin", +"Vorstand", +"tech");
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)] #[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
pub struct UserWithRolesAndMembershipPdf { pub struct UserWithRolesAndMembershipPdf {

View File

@@ -7,11 +7,11 @@ use crate::{
mail::valid_mails, mail::valid_mails,
role::Role, role::Role,
user::{ user::{
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member, clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser, regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser, schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
}, },
}, },
tera::Config, tera::Config,
@@ -19,6 +19,7 @@ use crate::{
use chrono::NaiveDate; use chrono::NaiveDate;
use futures::future::join_all; use futures::future::join_all;
use rocket::{ use rocket::{
FromForm, Request, Route, State,
form::Form, form::Form,
fs::TempFile, fs::TempFile,
get, get,
@@ -26,9 +27,9 @@ use rocket::{
post, post,
request::{FlashMessage, FromRequest, Outcome}, request::{FlashMessage, FromRequest, Outcome},
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, FromForm, Request, Route, State, routes,
}; };
use rocket_dyn_templates::{tera::Context, Template}; use rocket_dyn_templates::{Template, tera::Context};
use sqlx::SqlitePool; use sqlx::SqlitePool;
// Custom request guard to extract the Referer header // Custom request guard to extract the Referer header
@@ -356,7 +357,7 @@ async fn add_note(
match user.add_note(db, &admin, &data.note).await { match user.add_note(db, &admin, &data.note).await {
Ok(_) => Flash::success( Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)), Redirect::to(format!("/admin/user/{}", user.id)),
"Notiz hinzugefügt. Du findest sie ab sofort unter 'Aktivitäten'.", "Notiz hinzugefügt",
), ),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
} }

View File

@@ -1,16 +1,17 @@
use crate::model::{ use crate::model::{
boat::Boat, boat::Boat,
boathouse::Boathouse, boathouse::Boathouse,
user::{AllowedToUpdateBoathouse, UserWithDetails, VorstandUser}, user::{AdminUser, UserWithDetails, VorstandUser},
}; };
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, FromForm, Route, State, routes,
}; };
use rocket_dyn_templates::{tera::Context, Template}; use rocket_dyn_templates::{Template, tera::Context};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/boathouse")] #[get("/boathouse")]
@@ -37,11 +38,6 @@ async fn index(
let boathouse = Boathouse::get(db).await; let boathouse = Boathouse::get(db).await;
context.insert("boathouse", &boathouse); context.insert("boathouse", &boathouse);
let allowed_to_edit = AllowedToUpdateBoathouse::new(db, &admin.user)
.await
.is_some();
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithDetails::from_user(admin.into_inner(), db).await, &UserWithDetails::from_user(admin.into_inner(), db).await,
@@ -61,29 +57,36 @@ pub struct FormBoathouseToAdd {
async fn new<'r>( async fn new<'r>(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<FormBoathouseToAdd>, data: Form<FormBoathouseToAdd>,
user: AllowedToUpdateBoathouse, _admin: AdminUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match Boathouse::create(db, &user, data.into_inner()).await { match Boathouse::create(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/board/boathouse"), "Boot hinzugefügt"), Ok(_) => Flash::success(Redirect::to("/board/boathouse"), "Boot hinzugefügt"),
Err(e) => Flash::error(Redirect::to("/board/boathouse"), e), Err(e) => Flash::error(Redirect::to("/board/boathouse"), e),
} }
} }
#[get("/boathouse/<boathouse_id>/delete")] #[get("/boathouse/<boathouse_id>/delete")]
async fn delete( async fn delete(db: &State<SqlitePool>, _admin: AdminUser, boathouse_id: i32) -> Flash<Redirect> {
db: &State<SqlitePool>,
user: AllowedToUpdateBoathouse,
boathouse_id: i32,
) -> Flash<Redirect> {
let boat = Boathouse::find_by_id(db, boathouse_id).await; let boat = Boathouse::find_by_id(db, boathouse_id).await;
match boat { match boat {
Some(boat) => { Some(boat) => {
boat.delete(db, &user).await; boat.delete(db).await;
Flash::success(Redirect::to("/board/boathouse"), "Bootsplatz gelöscht") Flash::success(Redirect::to("/board/boathouse"), "Bootsplatz gelöscht")
} }
None => Flash::error(Redirect::to("/board/boathouse"), "Boatplace does not exist"), None => Flash::error(Redirect::to("/board/boathouse"), "Boatplace does not exist"),
} }
} }
//#[post("/boat/new", data = "<data>")]
//async fn create(
// db: &State<SqlitePool>,
// data: Form<BoatToAdd<'_>>,
// _admin: AdminUser,
//) -> Flash<Redirect> {
// match Boat::create(db, data.into_inner()).await {
// Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
// Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
// }
//}
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, new, delete] routes![index, new, delete]

View File

@@ -1,7 +1,8 @@
use std::env; use std::env;
use chrono::{Datelike, Utc}; use chrono::Utc;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
fs::TempFile, fs::TempFile,
get, get,
@@ -9,19 +10,18 @@ use rocket::{
post, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, FromForm, Route, State, routes,
}; };
use rocket_dyn_templates::{context, Template}; use rocket_dyn_templates::{Template, context};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::model::{ use crate::model::{
activity::ActivityBuilder,
log::Log, log::Log,
notification::Notification, notification::Notification,
role::Role, role::Role,
user::{AdminUser, ErgoAdminUser, User, UserWithDetails}, user::{AdminUser, User, UserWithDetails},
}; };
#[derive(Serialize)] #[derive(Serialize)]
@@ -59,7 +59,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
} }
#[get("/reset")] #[get("/reset")]
async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> Flash<Redirect> { async fn reset(db: &State<SqlitePool>, _user: AdminUser) -> Flash<Redirect> {
sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;") sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;")
.execute(db.inner()) .execute(db.inner())
.await .await
@@ -74,7 +74,7 @@ async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> Flash<Redirect>
#[get("/<challenge>/user/<user_id>/new?<new>")] #[get("/<challenge>/user/<user_id>/new?<new>")]
async fn update( async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
_admin: ErgoAdminUser, _admin: AdminUser,
challenge: &str, challenge: &str,
user_id: i64, user_id: i64,
new: &str, new: &str,
@@ -146,61 +146,47 @@ pub struct UserAdd {
sex: String, sex: String,
} }
#[post("/set-data", data = "<data>")] //#[post("/set-data", data = "<data>")]
async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> { //async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
if user.has_role(db, "ergo").await { // if user.has_role(db, "ergo").await {
return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei info@rudernlinz.at"); // return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at");
} // }
//
// check data // // check data
if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 { // if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 {
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr..."); // return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr...");
} // }
if data.weight < 20 || data.weight > 200 { // if data.weight < 20 || data.weight > 200 {
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht..."); // return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht...");
} // }
if &data.sex != "f" && &data.sex != "m" { // if &data.sex != "f" && &data.sex != "m" {
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht..."); // return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht...");
} // }
//
// set data // // set data
user.update_ergo(db, data.birthyear, data.weight, &data.sex) // user.update_ergo(db, data.birthyear, data.weight, &data.sex)
.await; // .await;
//
// inform all other `ergo` users // // inform all other `ergo` users
let ergo = Role::find_by_name(db, "ergo").await.unwrap(); // let ergo = Role::find_by_name(db, "ergo").await.unwrap();
Notification::create_for_role( // Notification::create_for_role(
db, // db,
&ergo, // &ergo,
&format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name), // &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name),
"Ergo Challenge", // "Ergo Challenge",
None, // None,
None, // None,
) // )
.await; // .await;
//
// add to `ergo` group // // add to `ergo` group
sqlx::query!( // user.add_role(db, &ergo).await.unwrap();
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", //
user.id, // Flash::success(
ergo.id // Redirect::to("/ergo"),
) // "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
.execute(db.inner()) // )
.await //}
.unwrap();
ActivityBuilder::new(&format!(
"{user} nimmt an der Ergo-Challenge teil und hat gerade die Daten eingegeben."
))
.user(&user)
.save(db)
.await;
Flash::success(
Redirect::to("/ergo"),
"Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
)
}
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
pub struct ErgoToAdd<'a> { pub struct ErgoToAdd<'a> {
@@ -373,7 +359,10 @@ async fn new_dozen(
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, new_thirty, new_dozen, send, reset, update, new_user] routes![
index, new_thirty, new_dozen, send, reset, update,
// new_user
]
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -47,46 +47,12 @@ impl<'r> FromRequest<'r> for KioskCookie {
} }
#[get("/", rank = 2)] #[get("/", rank = 2)]
async fn index_loggedin( async fn index(
db: &State<SqlitePool>, db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>, flash: Option<FlashMessage<'_>>,
user: DonauLinzUser, user: DonauLinzUser,
) -> Template { ) -> Template {
let mut context = Context::new();
let boats = Boat::for_user(db, &user).await; let boats = Boat::for_user(db, &user).await;
context.insert("boats", &boats);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into_inner(), db).await,
);
let context = index(db, flash, context).await;
Template::render("log", context.into_json())
}
#[get("/")]
async fn index_kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
) -> Template {
let mut context = Context::new();
let boats = Boat::all(db).await;
context.insert("boats", &boats);
context.insert("show_kiosk_header", &true);
let context = index(db, flash, context).await;
Template::render("kiosk", context.into_json())
}
async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Context) -> Context {
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
let mut coxes: Vec<UserWithDetails> = futures::future::join_all( let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
User::cox(db) User::cox(db)
@@ -95,7 +61,9 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
.map(|user| UserWithDetails::from_user(user, db)), .map(|user| UserWithDetails::from_user(user, db)),
) )
.await; .await;
coxes.retain(|u| u.roles.contains(&"Donau Linz".into())); coxes.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let mut users: Vec<UserWithDetails> = futures::future::join_all( let mut users: Vec<UserWithDetails> = futures::future::join_all(
User::all(db) User::all(db)
@@ -104,13 +72,23 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
.map(|user| UserWithDetails::from_user(user, db)), .map(|user| UserWithDetails::from_user(user, db)),
) )
.await; .await;
users.retain(|u| u.allowed_to_row()); users.retain(|u| {
u.roles.contains(&"Donau Linz".into())
|| u.roles.contains(&"scheckbuch".into())
|| u.user.name == "Externe Steuerperson"
});
let logtypes = LogType::all(db).await; let logtypes = LogType::all(db).await;
let distances = Distance::all(db).await; let distances = Distance::all(db).await;
let on_water = Logbook::on_water(db).await; let on_water = Logbook::on_water(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("boats", &boats);
context.insert("planned_trips", &Trip::get_for_today(db).await); context.insert("planned_trips", &Trip::get_for_today(db).await);
context.insert( context.insert(
"reservations", "reservations",
@@ -119,10 +97,14 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
context.insert("coxes", &coxes); context.insert("coxes", &coxes);
context.insert("users", &users); context.insert("users", &users);
context.insert("logtypes", &logtypes); context.insert("logtypes", &logtypes);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into_inner(), db).await,
);
context.insert("on_water", &on_water); context.insert("on_water", &on_water);
context.insert("distances", &distances); context.insert("distances", &distances);
context Template::render("log", context.into_json())
} }
#[get("/show", rank = 3)] #[get("/show", rank = 3)]
@@ -197,6 +179,63 @@ async fn new_kiosk(
Redirect::to("/log") Redirect::to("/log")
} }
#[get("/")]
async fn kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
) -> Template {
let boats = Boat::all(db).await;
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
User::cox(db)
.await
.into_iter()
.map(|user| UserWithDetails::from_user(user, db)),
)
.await;
coxes.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let mut users: Vec<UserWithDetails> = futures::future::join_all(
User::all(db)
.await
.into_iter()
.map(|user| UserWithDetails::from_user(user, db)),
)
.await;
users.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let logtypes = LogType::all(db).await;
let distances = Distance::all(db).await;
let on_water = Logbook::on_water(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("planned_trips", &Trip::get_for_today(db).await);
context.insert("boats", &boats);
context.insert(
"reservations",
&BoatReservation::all_future_with_groups(db).await,
);
context.insert("coxes", &coxes);
context.insert("users", &users);
context.insert("logtypes", &logtypes);
context.insert("on_water", &on_water);
context.insert("distances", &distances);
context.insert("show_kiosk_header", &true);
Template::render("kiosk", context.into_json())
}
async fn create_logbook( async fn create_logbook(
db: &SqlitePool, db: &SqlitePool,
data: Form<LogToAdd>, data: Form<LogToAdd>,
@@ -529,11 +568,11 @@ async fn delete_kiosk(
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![
index_loggedin, index,
index_kiosk,
create, create,
create_kiosk, create_kiosk,
home, home,
kiosk,
home_kiosk, home_kiosk,
new_kiosk, new_kiosk,
show, show,

View File

@@ -2,7 +2,7 @@ use std::{fs::OpenOptions, io::Write};
use chrono::{Datelike, Local}; use chrono::{Datelike, Local};
use rocket::{ use rocket::{
catch, catchers, Build, Data, FromForm, Request, Rocket, State, catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind}, fairing::{AdHoc, Fairing, Info, Kind},
form::Form, form::Form,
fs::FileServer, fs::FileServer,
@@ -13,7 +13,6 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
Build, Data, FromForm, Request, Rocket, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::Deserialize; use serde::Deserialize;
@@ -21,6 +20,7 @@ use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::{
SCHECKBUCH,
model::{ model::{
logbook::Logbook, logbook::Logbook,
notification::Notification, notification::Notification,
@@ -28,7 +28,6 @@ use crate::{
role::Role, role::Role,
user::{User, UserWithDetails}, user::{User, UserWithDetails},
}, },
SCHECKBUCH,
}; };
pub(crate) mod admin; pub(crate) mod admin;
@@ -331,11 +330,13 @@ mod test {
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert!(response assert!(
response
.into_string() .into_string()
.await .await
.unwrap() .unwrap()
.contains("Ruderassistent")); .contains("Ruderassistent")
);
} }
#[sqlx::test] #[sqlx::test]

View File

@@ -7,12 +7,12 @@
{% set place = boathouse[aisle_name][side_name].boats %} {% set place = boathouse[aisle_name][side_name].boats %}
{% if place[level] %} {% if place[level] %}
{{ place[level].boat.name }} {{ place[level].boat.name }}
{% if allowed_to_edit %} {% if "admin" in loggedin_user.roles %}
<a class="btn btn-primary absolute end-0" <a class="btn btn-primary absolute end-0"
href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a> href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a>
{% endif %} {% endif %}
{% elif boats | length > 0 %} {% elif boats | length > 0 %}
{% if allowed_to_edit %} {% if "admin" in loggedin_user.roles %}
<details> <details>
<summary>Kein Boot</summary> <summary>Kein Boot</summary>
<form action="/board/boathouse" method="post" class="grid gap-3"> <form action="/board/boathouse" method="post" class="grid gap-3">

View File

@@ -15,7 +15,10 @@
class="link-primary">Überblick der Challenges</a> class="link-primary">Überblick der Challenges</a>
</li> </li>
<li class="py-1"> <li class="py-1">
Eintragung ist jederzeit möglich, wenn du sie auch an die offizielle Liste schicken willst, kannst du das <a href="https://data.ergochallenge.at/" target="_blank" style="text-decoration: underline">hier</a> machen Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt
<li class="py-1">
Montag &rarr; gemeinsames Training; bitte um <a href="/planned" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
</li>
<li class="py-1"> <li class="py-1">
<a href="https://data.ergochallenge.at" <a href="https://data.ergochallenge.at"
target="_blank" target="_blank"
@@ -191,7 +194,7 @@
</div> </div>
</details> </details>
</div> </div>
{% if "admin" in loggedin_user.roles or "ergo-admin" in loggedin_user.roles %} {% if "admin" in loggedin_user.roles %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
<h2 class="h2">Update</h2> <h2 class="h2">Update</h2>
<details class="p-2"> <details class="p-2">
@@ -230,14 +233,6 @@
</ol> </ol>
</div> </div>
</details> </details>
<div class="mt-3 text-right">
<a href="/ergo/reset"
class="w-28 btn btn-alert"
onclick="return confirm('Willst du wirklich alle Ergo-Eingaben löschen?');">
{% include "includes/delete-icon" %}
Einträge löschen
</a>
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}