forked from Ruderverein-Donau-Linz/rowt
first draft of normannen deployment
This commit is contained in:
@@ -1,651 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::model::boathouse::Boathouse;
|
||||
|
||||
use super::location::Location;
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Boat {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub amount_seats: i64,
|
||||
pub location_id: i64,
|
||||
pub owner: Option<i64>,
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<String>,
|
||||
pub default_destination: Option<String>,
|
||||
#[serde(default = "bool::default")]
|
||||
pub convert_handoperated_possible: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
skull: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
pub external: bool,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BoatDamage {
|
||||
None,
|
||||
Light,
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct BoatWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub(crate) boat: Boat,
|
||||
damage: BoatDamage,
|
||||
on_water: bool,
|
||||
reserved_today: bool,
|
||||
cat: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct BoatToAdd<'r> {
|
||||
pub name: &'r str,
|
||||
pub amount_seats: i64,
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<&'r str>,
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub external: bool,
|
||||
pub location_id: Option<i64>,
|
||||
pub owner: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct BoatToUpdate<'r> {
|
||||
pub name: &'r str,
|
||||
pub amount_seats: i64,
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<&'r str>,
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub external: bool,
|
||||
pub location_id: i64,
|
||||
pub owner: Option<i64>,
|
||||
}
|
||||
|
||||
impl Boat {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
if self.location_id == ottensheim.id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.amount_seats == 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.allowed_to_steer(db).await
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed_tx(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
) -> bool {
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if self.amount_seats == 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.allowed_to_steer_tx(db).await
|
||||
}
|
||||
|
||||
pub async fn is_locked(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=true AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
|
||||
}
|
||||
|
||||
pub async fn has_minor_damage(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
|
||||
}
|
||||
|
||||
pub async fn reserved_today(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT *
|
||||
FROM boat_reservation
|
||||
WHERE boat_id =?
|
||||
AND date('now') BETWEEN start_date AND end_date;",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn on_water(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT * FROM logbook WHERE boat_id=? AND arrival is null",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn cat(&self) -> String {
|
||||
if self.external {
|
||||
"Vereinsfremde Boote".to_string()
|
||||
} else if self.default_shipmaster_only_steering {
|
||||
format!("{}+", self.amount_seats - 1)
|
||||
} else {
|
||||
format!("{}x", self.amount_seats)
|
||||
}
|
||||
}
|
||||
|
||||
async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
|
||||
let mut res = Vec::new();
|
||||
for boat in boats {
|
||||
let mut damage = BoatDamage::None;
|
||||
if boat.has_minor_damage(db).await {
|
||||
damage = BoatDamage::Light;
|
||||
}
|
||||
if boat.is_locked(db).await {
|
||||
damage = BoatDamage::Locked;
|
||||
}
|
||||
let cat = boat.cat();
|
||||
|
||||
res.push(BoatWithDetails {
|
||||
damage,
|
||||
on_water: boat.on_water(db).await,
|
||||
reserved_today: boat.reserved_today(db).await,
|
||||
boat,
|
||||
cat,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<BoatWithDetails> {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE deleted=false
|
||||
ORDER BY amount_seats DESC
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec<BoatWithDetails> {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT
|
||||
b.id,
|
||||
b.name,
|
||||
b.amount_seats,
|
||||
b.location_id,
|
||||
b.owner,
|
||||
b.year_built,
|
||||
b.boatbuilder,
|
||||
b.default_shipmaster_only_steering,
|
||||
b.default_destination,
|
||||
b.skull,
|
||||
b.external,
|
||||
b.deleted,
|
||||
b.convert_handoperated_possible
|
||||
FROM
|
||||
boat AS b
|
||||
WHERE
|
||||
b.external = false
|
||||
AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
|
||||
AND b.deleted = false
|
||||
ORDER BY
|
||||
b.name DESC;
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
|
||||
if user.has_role(db, "admin").await {
|
||||
return Self::all(db).await;
|
||||
}
|
||||
let mut boats = if user.allowed_to_steer(db).await {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner is null or owner = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
};
|
||||
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let boats_in_ottensheim = sqlx::query_as!(
|
||||
Boat,
|
||||
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner is null and location_id = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",ottensheim.id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
boats.extend(boats_in_ottensheim.into_iter());
|
||||
}
|
||||
let boats = boats.into_iter().unique().collect();
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
INNER JOIN location ON boat.location_id = location.id
|
||||
WHERE location.name=? AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
location
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
boat.boatbuilder,
|
||||
boat.default_shipmaster_only_steering,
|
||||
boat.default_destination,
|
||||
boat.skull,
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
boat.boatbuilder,
|
||||
boat.default_shipmaster_only_steering,
|
||||
boat.default_destination,
|
||||
boat.skull,
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn owner(&self, db: &SqlitePool) -> Option<User> {
|
||||
if let Some(owner_id) = self.owner {
|
||||
Some(User::find_by_id(db, owner_id as i32).await.unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
|
||||
pub async fn boathouse(&self, db: &SqlitePool) -> Option<Boathouse> {
|
||||
sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT * FROM boathouse WHERE boat_id like ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn on_water_between(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
dep: NaiveDateTime,
|
||||
arr: NaiveDateTime,
|
||||
) -> bool {
|
||||
let dep = dep.format("%Y-%m-%dT%H:%M").to_string();
|
||||
let arr = arr.format("%Y-%m-%dT%H:%M").to_string();
|
||||
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS overlap_count
|
||||
FROM logbook
|
||||
WHERE boat_id = ?
|
||||
AND (
|
||||
(departure <= ? AND arrival >= ?) -- Existing entry covers the entire new period
|
||||
OR (departure >= ? AND departure < ?) -- Existing entry starts during the new period
|
||||
OR (arrival > ? AND arrival <= ?) -- Existing entry ends during the new period
|
||||
);",
|
||||
self.id,
|
||||
arr,
|
||||
arr,
|
||||
dep,
|
||||
dep,
|
||||
dep,
|
||||
arr
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
.overlap_count
|
||||
> 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::boat::{Boat, BoatToAdd},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::BoatToUpdate;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_correct_id() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(boat.id, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_wrong_id() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1337).await;
|
||||
assert!(boat.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_all() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::all(&pool).await;
|
||||
assert!(res.len() > 3);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(
|
||||
Boat::create(
|
||||
&pool,
|
||||
BoatToAdd {
|
||||
name: "new-boat-name".into(),
|
||||
amount_seats: 42,
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
owner: None,
|
||||
default_destination: None
|
||||
}
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_duplicate_name_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(
|
||||
Boat::create(
|
||||
&pool,
|
||||
BoatToAdd {
|
||||
name: "Haichenbach".into(),
|
||||
amount_seats: 42,
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
owner: None,
|
||||
default_destination: None
|
||||
}
|
||||
)
|
||||
.await,
|
||||
Err(
|
||||
"error returned from database: (code: 2067) UNIQUE constraint failed: boat.name"
|
||||
.into()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_is_locked() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 5)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_locked(&pool)
|
||||
.await;
|
||||
assert_eq!(res, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_is_not_locked() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 4)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_locked(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_is_not_locked_no_damage() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 3)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_locked(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_has_minor_damage() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 4)
|
||||
.await
|
||||
.unwrap()
|
||||
.has_minor_damage(&pool)
|
||||
.await;
|
||||
assert_eq!(res, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_has_no_minor_damage() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 5)
|
||||
.await
|
||||
.unwrap()
|
||||
.has_minor_damage(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_on_water() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 2)
|
||||
.await
|
||||
.unwrap()
|
||||
.on_water(&pool)
|
||||
.await;
|
||||
assert_eq!(res, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_not_on_water() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 4)
|
||||
.await
|
||||
.unwrap()
|
||||
.on_water(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_update() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
let update = BoatToUpdate {
|
||||
name: "my-new-boat-name",
|
||||
amount_seats: 3,
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 1,
|
||||
owner: None,
|
||||
default_destination: None,
|
||||
};
|
||||
|
||||
boat.update(&pool, update).await.unwrap();
|
||||
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(boat.name, "my-new-boat-name");
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_failed_update() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
let update = BoatToUpdate {
|
||||
name: "my-new-boat-name",
|
||||
amount_seats: 3,
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 999,
|
||||
owner: None,
|
||||
default_destination: None,
|
||||
};
|
||||
|
||||
match boat.update(&pool, update).await {
|
||||
Ok(_) => panic!("Update with invalid location should not succeed"),
|
||||
Err(e) => assert_eq!(
|
||||
e,
|
||||
"error returned from database: (code: 787) FOREIGN KEY constraint failed"
|
||||
),
|
||||
};
|
||||
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(boat.name, "Haichenbach");
|
||||
}
|
||||
}
|
@@ -1,350 +0,0 @@
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatDamage {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub desc: String,
|
||||
pub user_id_created: i64,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub user_id_fixed: Option<i64>,
|
||||
pub fixed_at: Option<NaiveDateTime>,
|
||||
pub user_id_verified: Option<i64>,
|
||||
pub verified_at: Option<NaiveDateTime>,
|
||||
pub lock_boat: bool,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatDamageWithDetails {
|
||||
#[serde(flatten)]
|
||||
boat_damage: BoatDamage,
|
||||
user_created: User,
|
||||
user_fixed: Option<User>,
|
||||
user_verified: Option<User>,
|
||||
boat: Boat,
|
||||
verified: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoatDamageToAdd<'r> {
|
||||
pub boat_id: i64,
|
||||
pub desc: &'r str,
|
||||
pub user_id_created: i32,
|
||||
pub lock_boat: bool,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct BoatDamageFixed<'r> {
|
||||
pub desc: &'r str,
|
||||
pub user_id_fixed: i32,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct BoatDamageVerified<'r> {
|
||||
pub desc: &'r str,
|
||||
pub user_id_verified: i32,
|
||||
}
|
||||
|
||||
impl BoatDamage {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
|
||||
FROM boat_damage
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<BoatDamageWithDetails> {
|
||||
let boatdamages = sqlx::query_as!(
|
||||
BoatDamage,
|
||||
"
|
||||
SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
|
||||
FROM boat_damage
|
||||
WHERE (
|
||||
verified_at IS NULL
|
||||
OR verified_at >= datetime('now', '-30 days')
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for boat_damage in boatdamages {
|
||||
let user_fixed = match boat_damage.user_id_fixed {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_verified = match boat_damage.user_id_verified {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
res.push(BoatDamageWithDetails {
|
||||
boat: Boat::find_by_id(db, boat_damage.boat_id as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
user_created: User::find_by_id(db, boat_damage.user_id_created as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
user_fixed,
|
||||
verified: user_verified.is_some(),
|
||||
user_verified,
|
||||
boat_damage,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
|
||||
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
|
||||
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
|
||||
boatdamage.boat_id,
|
||||
boatdamage.desc,
|
||||
boatdamage.user_id_created,
|
||||
boatdamage.lock_boat
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if !was_unusable_before && boat.is_locked(db).await {
|
||||
Notification::create_for_steering_people(db, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
if technical.id as i32 != boatdamage.user_id_created {
|
||||
Notification::create(
|
||||
db,
|
||||
&technical,
|
||||
&format!(
|
||||
"{} hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
|
||||
User::find_by_id(db, boatdamage.user_id_created)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boat.name,
|
||||
boatdamage.desc
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&User::find_by_id(db, boatdamage.user_id_created)
|
||||
.await
|
||||
.unwrap(),
|
||||
&format!(
|
||||
"Du hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
|
||||
Boat::find_by_id(db, boatdamage.boat_id as i32)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boatdamage.desc
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fixed(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
boat_damage: BoatDamageFixed<'_>,
|
||||
) -> Result<(), String> {
|
||||
Log::create(db, format!("Fixed boat damage: {boat_damage:?}")).await;
|
||||
|
||||
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET desc=?, user_id_fixed=?, fixed_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
boat_damage.desc,
|
||||
boat_damage.user_id_fixed,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap();
|
||||
if user.has_role(db, "tech").await {
|
||||
return self
|
||||
.verified(
|
||||
db,
|
||||
BoatDamageVerified {
|
||||
desc: boat_damage.desc,
|
||||
user_id_verified: user.id as i32,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
if technical.id as i32 != boat_damage.user_id_fixed {
|
||||
Notification::create(
|
||||
db,
|
||||
&technical,
|
||||
&format!(
|
||||
"{} hat den Bootschaden '{}' beim Boot '{}' repariert. Könntest du das bei Gelegenheit verifizieren?",
|
||||
User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boat_damage.desc,
|
||||
boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if boat_damage.user_id_fixed != self.user_id_created as i32 {
|
||||
let user_fixed = User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Boatdamage is also directly verified, if a tech has repaired it. We don't want to
|
||||
// send 2 notifications.
|
||||
if !user_fixed.has_role(db, "tech").await {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert. Dieser muss nun noch von unseren Bootswarten bestätigt werden.",
|
||||
user_fixed.name,
|
||||
boat_damage.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verified(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
boat_form: BoatDamageVerified<'_>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await {
|
||||
if !verifier.has_role(db, "tech").await {
|
||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await;
|
||||
return Err("You are not allowed to verify the boat!".into());
|
||||
}
|
||||
} else {
|
||||
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await;
|
||||
return Err("Could not find user".into());
|
||||
}
|
||||
|
||||
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
boat_form.desc,
|
||||
boat_form.user_id_verified,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
|
||||
if boat_form.user_id_verified != self.user_id_created as i32 {
|
||||
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
|
||||
.await
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user_verified.id == self.user_id_fixed.unwrap() {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert und verifiziert.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert & verifiziert",
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat verifiziert, dass der von dir eingetragenen Bootschaden '{}' beim Boot '{}' korrekt repariert wurde.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden verifiziert",
|
||||
None,
|
||||
None
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
if was_unusable_before && !boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,120 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use crate::tera::board::boathouse::FormBoathouseToAdd;
|
||||
|
||||
use super::boat::Boat;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Boathouse {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub aisle: String,
|
||||
pub side: String,
|
||||
pub level: i64,
|
||||
}
|
||||
|
||||
impl Boathouse {
|
||||
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> {
|
||||
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
|
||||
|
||||
let mut mountain = HashMap::new();
|
||||
mountain.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
mountain.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("mountain-aisle", mountain);
|
||||
|
||||
let mut middle = HashMap::new();
|
||||
middle.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
middle.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("middle-aisle", middle);
|
||||
|
||||
let mut water = HashMap::new();
|
||||
water.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
water.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("water-aisle", water);
|
||||
|
||||
let boathouses = sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT id, boat_id, aisle, side, level FROM boathouse"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
for boathouse in boathouses {
|
||||
let aisle = ret
|
||||
.get_mut(format!("{}-aisle", boathouse.aisle).as_str())
|
||||
.unwrap();
|
||||
let side = aisle.get_mut(boathouse.side.as_str()).unwrap();
|
||||
|
||||
side[boathouse.level as usize] = Some((
|
||||
boathouse.id,
|
||||
Boat::find_by_id(db, boathouse.boat_id as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)",
|
||||
data.boat_id,
|
||||
data.aisle,
|
||||
data.side,
|
||||
data.level
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM boathouse WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
@@ -1,228 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use crate::tera::boatreservation::ReservationEditForm;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatReservation {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: String,
|
||||
pub usage: String,
|
||||
pub user_id_applicant: i64,
|
||||
pub user_id_confirmation: Option<i64>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: BoatReservation,
|
||||
boat: Boat,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoatReservationToAdd<'r> {
|
||||
pub boat: &'r Boat,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl BoatReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
|
||||
let boatreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for reservation in boatreservations {
|
||||
let user_confirmation = match reservation.user_id_confirmation {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let boat = Boat::find_by_id(db, reservation.boat_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(BoatReservationWithDetails {
|
||||
reservation,
|
||||
boat,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
reservation.reservation.start_date,
|
||||
reservation.reservation.end_date,
|
||||
reservation.reservation.time_desc,
|
||||
reservation.reservation.usage,
|
||||
reservation.user_applicant.name
|
||||
);
|
||||
|
||||
grouped_reservations
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(reservation);
|
||||
}
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
boatreservation: BoatReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::boat_reserved_between_dates(
|
||||
db,
|
||||
boatreservation.boat,
|
||||
&boatreservation.start_date,
|
||||
&boatreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Boot in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(db, format!("New boat reservation: {boatreservation:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
boatreservation.boat.id,
|
||||
boatreservation.start_date,
|
||||
boatreservation.end_date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage,
|
||||
boatreservation.user_applicant.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let board =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
|
||||
for user in board {
|
||||
let date = if boatreservation.start_date == boatreservation.end_date {
|
||||
format!("am {}", boatreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
boatreservation.start_date, boatreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Bootsreservierung für Boot '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
boatreservation.user_applicant.name,
|
||||
boatreservation.boat.name,
|
||||
date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage
|
||||
),
|
||||
"Neue Bootsreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn boat_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
boat: &Boat,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM boat_reservation
|
||||
WHERE boat_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
boat.id,
|
||||
end_date,
|
||||
start_date
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.reservation_count
|
||||
> 0
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
|
||||
let time_desc = data.time_desc.trim();
|
||||
let usage = data.usage.trim();
|
||||
sqlx::query!(
|
||||
"UPDATE boat_reservation SET time_desc = ?, usage = ? where id = ?",
|
||||
time_desc,
|
||||
usage,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM boat_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone, Debug)]
|
||||
pub struct Distance {
|
||||
pub id: i64,
|
||||
pub destination: String,
|
||||
pub distance_in_km: i64,
|
||||
}
|
||||
|
||||
impl Distance {
|
||||
/// Return all default `distance`s, ordered by usage in logbook entries
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT
|
||||
d.id,
|
||||
d.destination,
|
||||
d.distance_in_km
|
||||
FROM
|
||||
distance d
|
||||
LEFT JOIN
|
||||
logbook l ON d.destination = l.destination AND d.distance_in_km = l.distance_in_km
|
||||
GROUP BY
|
||||
d.id, d.destination, d.distance_in_km
|
||||
ORDER BY
|
||||
COUNT(l.id) DESC, d.destination ASC;"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
@@ -1,94 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Family {
|
||||
id: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct FamilyWithMembers {
|
||||
id: i64,
|
||||
names: Option<String>,
|
||||
}
|
||||
|
||||
impl Family {
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id FROM role")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn insert_tx(db: &mut Transaction<'_, Sqlite>) -> i64 {
|
||||
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
result.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub async fn insert(db: &SqlitePool) -> i64 {
|
||||
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
result.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub async fn all_with_members(db: &SqlitePool) -> Vec<FamilyWithMembers> {
|
||||
sqlx::query_as!(
|
||||
FamilyWithMembers,
|
||||
"
|
||||
SELECT
|
||||
family.id as id,
|
||||
GROUP_CONCAT(user.name, ', ') as names
|
||||
FROM family
|
||||
LEFT JOIN
|
||||
user ON family.id = user.family_id
|
||||
GROUP BY family.id;"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_opt_id(db: &SqlitePool, id: Option<i64>) -> Option<Self> {
|
||||
if let Some(id) = id {
|
||||
Self::find_by_id(db, id).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.count
|
||||
}
|
||||
|
||||
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
|
||||
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
@@ -1,103 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Location {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM location
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM location
|
||||
WHERE name=?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM location")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
||||
sqlx::query!("INSERT INTO location(name) VALUES (?)", name)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM location WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Location of a valid id
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{model::location::Location, testdb};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_correct_id() {
|
||||
let pool = testdb!();
|
||||
let location = Location::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(location.id, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_wrong_id() {
|
||||
let pool = testdb!();
|
||||
let location = Location::find_by_id(&pool, 1337).await;
|
||||
assert!(location.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_all() {
|
||||
let pool = testdb!();
|
||||
let res = Location::all(&pool).await;
|
||||
assert!(res.len() > 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(Location::create(&pool, "new-loc-name".into(),).await, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_duplicate_name_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(Location::create(&pool, "Linz".into(),).await, false);
|
||||
}
|
||||
}
|
1287
src/model/logbook.rs
1287
src/model/logbook.rs
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct LogType {
|
||||
pub id: i64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl LogType {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM logbook_type
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM logbook_type
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::testdb;
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_true() {
|
||||
let _ = testdb!();
|
||||
}
|
||||
|
||||
//TODO: write tests
|
||||
}
|
@@ -1,367 +0,0 @@
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use lettre::{
|
||||
message::{header::ContentType, Attachment, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
use super::{family::Family, log::Log, role::Role, user::User};
|
||||
|
||||
pub struct Mail {}
|
||||
|
||||
impl Mail {
|
||||
pub async fn send_single(
|
||||
db: &SqlitePool,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: String,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::send_single_tx(&mut tx, to, subject, body, smtp_pw).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn send_single_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: String,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = to.split(',');
|
||||
for single_rec in splitted {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create_with_tx(
|
||||
db,
|
||||
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||
)
|
||||
.await;
|
||||
return Err(format!(
|
||||
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let email = email
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let role = Role::find_by_id(db, data.role_id).await.unwrap();
|
||||
for rec in role.mails_from_role(db).await {
|
||||
let splitted = rec.split(',');
|
||||
for single_rec in splitted {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("Mail not sent to {rec}, because it could not be parsed"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body));
|
||||
|
||||
for temp_file in &data.files {
|
||||
let content = fs::read(temp_file.path().unwrap()).unwrap();
|
||||
let media_type = format!("{}", temp_file.content_type().unwrap().media_type());
|
||||
let content_type = ContentType::parse(&media_type).unwrap();
|
||||
if let Some(name) = temp_file.name() {
|
||||
let attachment = Attachment::new(format!(
|
||||
"{}.{}",
|
||||
name,
|
||||
temp_file.content_type().unwrap().extension().unwrap()
|
||||
))
|
||||
.body(content, content_type);
|
||||
|
||||
multipart = multipart.singlepart(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
let email = email.subject(data.subject).multipart(multipart).unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => return true,
|
||||
Err(e) => println!("{:?}", e.source()),
|
||||
};
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn fees(db: &SqlitePool, smtp_pw: String) {
|
||||
let users = User::all_payer_groups(db).await;
|
||||
for user in users {
|
||||
if !user.has_role(db, "paid").await {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
Some(family) => {
|
||||
is_family = true;
|
||||
for member in family.members(db).await {
|
||||
if let Some(mail) = member.mail {
|
||||
send_to.push_str(&format!("{mail},"))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(mail) = &user.mail {
|
||||
send_to.push_str(mail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fees = user.fee(db).await;
|
||||
if let Some(fees) = fees {
|
||||
let mut content = format!(
|
||||
"Liebes Vereinsmitglied, \n\n\
|
||||
dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.sum_in_cents / 100,
|
||||
);
|
||||
|
||||
if fees.parts.len() == 1 {
|
||||
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
|
||||
} else {
|
||||
content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
|
||||
for (desc, fee_in_cents) in fees.parts {
|
||||
content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100))
|
||||
}
|
||||
}
|
||||
if is_family {
|
||||
content.push_str(&format!(
|
||||
"Dieser gilt für die gesamte Familie ({}).\n",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
|
||||
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
|
||||
Beste Grüße\n\
|
||||
Der Vorstand
|
||||
");
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = send_to.split(',');
|
||||
let mut send_mail = false;
|
||||
for single_rec in splitted {
|
||||
let single_rec = single_rec.trim();
|
||||
match single_rec.parse() {
|
||||
Ok(val) => {
|
||||
email = email.bcc(val);
|
||||
send_mail = true;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Error in mail: {single_rec}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send_mail {
|
||||
let email = email
|
||||
.subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(content)
|
||||
.unwrap();
|
||||
|
||||
let creds =
|
||||
Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone());
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fees_final(db: &SqlitePool, smtp_pw: String) {
|
||||
let users = User::all_payer_groups(db).await;
|
||||
for user in users {
|
||||
if let Some(fee) = user.fee(db).await {
|
||||
if !fee.paid {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
Some(family) => {
|
||||
is_family = true;
|
||||
for member in family.members(db).await {
|
||||
if let Some(mail) = member.mail {
|
||||
send_to.push_str(&format!("{mail},"))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(mail) = &user.mail {
|
||||
send_to.push_str(mail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fees = user.fee(db).await;
|
||||
if let Some(fees) = fees {
|
||||
let mut content = format!(
|
||||
"Liebes Vereinsmitglied, \n\n\
|
||||
wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können.
|
||||
|
||||
Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\
|
||||
Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.sum_in_cents / 100,
|
||||
);
|
||||
|
||||
if fees.parts.len() == 1 {
|
||||
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
|
||||
} else {
|
||||
content
|
||||
.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
|
||||
for (desc, fee_in_cents) in fees.parts {
|
||||
content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100))
|
||||
}
|
||||
}
|
||||
if is_family {
|
||||
content.push_str(&format!(
|
||||
"Dieser gilt für die gesamte Familie ({}).\n",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\n\
|
||||
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
|
||||
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
|
||||
|
||||
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||
|
||||
Mit freundlichen Grüßen,\n\
|
||||
Der Vorstand");
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = send_to.split(',');
|
||||
let mut send_mail = false;
|
||||
for single_rec in splitted {
|
||||
let single_rec = single_rec.trim();
|
||||
match single_rec.parse() {
|
||||
Ok(val) => {
|
||||
email = email.bcc(val);
|
||||
send_mail = true;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Error in mail: {single_rec}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send_mail {
|
||||
let email = email
|
||||
.subject("Mahnung Vereinsgebühren | ASKÖ Ruderverein Donau Linz")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(content)
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new(
|
||||
"no-reply@rudernlinz.at".to_owned(),
|
||||
smtp_pw.clone(),
|
||||
);
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,25 +12,11 @@ use self::{
|
||||
weather::Weather,
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod distance;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
pub mod log;
|
||||
pub mod logbook;
|
||||
pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod personal;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
pub mod trailer;
|
||||
pub mod trailerreservation;
|
||||
pub mod trip;
|
||||
pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
|
@@ -203,14 +203,6 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_by_link(db: &sqlx::Pool<Sqlite>, link: &str) {
|
||||
let link = Some(link);
|
||||
sqlx::query!("DELETE FROM notification WHERE link=?", link)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@@ -1,87 +0,0 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, PartialEq, Debug)]
|
||||
pub(crate) enum Level {
|
||||
NONE,
|
||||
BRONZE,
|
||||
SILVER,
|
||||
GOLD,
|
||||
DIAMOND,
|
||||
DONE,
|
||||
}
|
||||
|
||||
impl Level {
|
||||
fn required_km(&self) -> i32 {
|
||||
match self {
|
||||
Level::BRONZE => 40000,
|
||||
Level::SILVER => 80000,
|
||||
Level::GOLD => 100000,
|
||||
Level::DIAMOND => 200000,
|
||||
Level::DONE => 0,
|
||||
Level::NONE => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_level(km: i32) -> Self {
|
||||
if km < Level::BRONZE.required_km() {
|
||||
Level::BRONZE
|
||||
} else if km < Level::SILVER.required_km() {
|
||||
Level::SILVER
|
||||
} else if km < Level::GOLD.required_km() {
|
||||
Level::GOLD
|
||||
} else if km < Level::DIAMOND.required_km() {
|
||||
Level::DIAMOND
|
||||
} else {
|
||||
Level::DONE
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn curr_level(km: i32) -> Self {
|
||||
if km < Level::BRONZE.required_km() {
|
||||
Level::NONE
|
||||
} else if km < Level::SILVER.required_km() {
|
||||
Level::BRONZE
|
||||
} else if km < Level::GOLD.required_km() {
|
||||
Level::SILVER
|
||||
} else if km < Level::DIAMOND.required_km() {
|
||||
Level::GOLD
|
||||
} else {
|
||||
Level::DIAMOND
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn desc(&self) -> &str {
|
||||
match self {
|
||||
Level::BRONZE => "Bronze",
|
||||
Level::SILVER => "Silber",
|
||||
Level::GOLD => "Gold",
|
||||
Level::DIAMOND => "Diamant",
|
||||
Level::DONE => "",
|
||||
Level::NONE => "-",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct Next {
|
||||
level: Level,
|
||||
desc: String,
|
||||
missing_km: i32,
|
||||
required_km: i32,
|
||||
rowed_km: i32,
|
||||
}
|
||||
|
||||
impl Next {
|
||||
pub(crate) fn new(rowed_km: i32) -> Self {
|
||||
let level = Level::next_level(rowed_km);
|
||||
let required_km = level.required_km();
|
||||
let missing_km = required_km - rowed_km;
|
||||
Self {
|
||||
desc: level.desc().to_string(),
|
||||
level,
|
||||
missing_km,
|
||||
required_km,
|
||||
rowed_km,
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,52 +1 @@
|
||||
use chrono::{Datelike, Local};
|
||||
use equatorprice::Level;
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{logbook::Logbook, stat::Stat, user::User};
|
||||
|
||||
pub(crate) mod cal;
|
||||
pub(crate) mod equatorprice;
|
||||
pub(crate) mod rowingbadge;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct Achievements {
|
||||
pub(crate) equatorprice: equatorprice::Next,
|
||||
pub(crate) curr_equatorprice_name: String,
|
||||
pub(crate) new_equatorprice_this_season: bool,
|
||||
pub(crate) rowingbadge: Option<rowingbadge::Status>,
|
||||
pub(crate) all_time_km: i32,
|
||||
pub(crate) year_first_mentioned: Option<i32>,
|
||||
pub(crate) year_last_mentioned: Option<i32>,
|
||||
}
|
||||
|
||||
impl Achievements {
|
||||
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
|
||||
let rowed_km = Stat::total_km(db, user).await.rowed_km;
|
||||
let rowed_km_this_season = if Local::now().month() == 1 {
|
||||
Stat::person(db, Some(Local::now().year() - 1), user)
|
||||
.await
|
||||
.rowed_km
|
||||
+ Stat::person(db, Some(Local::now().year()), user)
|
||||
.await
|
||||
.rowed_km
|
||||
} else {
|
||||
Stat::person(db, Some(Local::now().year()), user)
|
||||
.await
|
||||
.rowed_km
|
||||
};
|
||||
|
||||
let new_equatorprice_this_season =
|
||||
Level::curr_level(rowed_km) != Level::curr_level(rowed_km - rowed_km_this_season);
|
||||
|
||||
Self {
|
||||
equatorprice: equatorprice::Next::new(rowed_km),
|
||||
curr_equatorprice_name: equatorprice::Level::curr_level(rowed_km).desc().to_string(),
|
||||
new_equatorprice_this_season,
|
||||
rowingbadge: rowingbadge::Status::for_user(db, user).await,
|
||||
all_time_km: rowed_km,
|
||||
year_first_mentioned: Logbook::year_first_logbook_entry(db, user).await,
|
||||
year_last_mentioned: Logbook::year_last_logbook_entry(db, user).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,166 +0,0 @@
|
||||
use std::cmp;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
|
||||
stat::Stat,
|
||||
user::User,
|
||||
};
|
||||
|
||||
enum AgeBracket {
|
||||
Till14,
|
||||
From14Till18,
|
||||
From19Till30,
|
||||
From31Till60,
|
||||
From61Till75,
|
||||
From76,
|
||||
}
|
||||
|
||||
impl AgeBracket {
|
||||
fn cat(&self) -> &str {
|
||||
match self {
|
||||
AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre",
|
||||
AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre",
|
||||
AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre",
|
||||
AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre",
|
||||
AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre",
|
||||
AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre",
|
||||
}
|
||||
}
|
||||
|
||||
fn dist_in_km(&self) -> i32 {
|
||||
match self {
|
||||
AgeBracket::Till14 => 500,
|
||||
AgeBracket::From14Till18 => 1000,
|
||||
AgeBracket::From19Till30 => 1200,
|
||||
AgeBracket::From31Till60 => 1000,
|
||||
AgeBracket::From61Till75 => 800,
|
||||
AgeBracket::From76 => 600,
|
||||
}
|
||||
}
|
||||
|
||||
fn required_dist_multi_day_in_km(&self) -> i32 {
|
||||
match self {
|
||||
AgeBracket::Till14 => 60,
|
||||
AgeBracket::From14Till18 => 60,
|
||||
AgeBracket::From19Till30 => 80,
|
||||
AgeBracket::From31Till60 => 80,
|
||||
AgeBracket::From61Till75 => 80,
|
||||
AgeBracket::From76 => 80,
|
||||
}
|
||||
}
|
||||
|
||||
fn required_dist_single_day_in_km(&self) -> i32 {
|
||||
match self {
|
||||
AgeBracket::Till14 => 30,
|
||||
AgeBracket::From14Till18 => 30,
|
||||
AgeBracket::From19Till30 => 40,
|
||||
AgeBracket::From31Till60 => 40,
|
||||
AgeBracket::From61Till75 => 40,
|
||||
AgeBracket::From76 => 40,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&User> for AgeBracket {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &User) -> Result<Self, Self::Error> {
|
||||
let Some(birthdate) = value.birthdate.clone() else {
|
||||
return Err("User has no birthdate".to_string());
|
||||
};
|
||||
|
||||
let Ok(birthdate) = NaiveDate::parse_from_str(&birthdate, "%Y-%m-%d") else {
|
||||
return Err("Birthdate in wrong format...".to_string());
|
||||
};
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
|
||||
let age = today.year() - birthdate.year();
|
||||
if age <= 14 {
|
||||
Ok(AgeBracket::Till14)
|
||||
} else if age <= 18 {
|
||||
Ok(AgeBracket::From14Till18)
|
||||
} else if age <= 30 {
|
||||
Ok(AgeBracket::From19Till30)
|
||||
} else if age <= 60 {
|
||||
Ok(AgeBracket::From31Till60)
|
||||
} else if age <= 75 {
|
||||
Ok(AgeBracket::From61Till75)
|
||||
} else {
|
||||
Ok(AgeBracket::From76)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct Status {
|
||||
pub(crate) year: i32,
|
||||
rowed_km: i32,
|
||||
category: String,
|
||||
required_km: i32,
|
||||
missing_km: i32,
|
||||
multi_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
|
||||
multi_day_trips_required_distance: i32,
|
||||
single_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
|
||||
single_day_trips_required_distance: i32,
|
||||
achieved: bool,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
|
||||
let Ok(agebracket) = AgeBracket::try_from(user) else {
|
||||
return None;
|
||||
};
|
||||
let category = agebracket.cat().to_string();
|
||||
|
||||
let year = if Local::now().month() == 1 {
|
||||
Local::now().year() - 1
|
||||
} else {
|
||||
Local::now().year()
|
||||
};
|
||||
|
||||
let rowed_km = Stat::person(db, Some(year), user).await.rowed_km;
|
||||
let required_km = agebracket.dist_in_km();
|
||||
let missing_km = cmp::max(required_km - rowed_km, 0);
|
||||
|
||||
let single_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_single_day_in_km(),
|
||||
year,
|
||||
Filter::SingleDayOnly,
|
||||
)
|
||||
.await;
|
||||
let multi_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_multi_day_in_km(),
|
||||
year,
|
||||
Filter::MultiDazOnly,
|
||||
)
|
||||
.await;
|
||||
|
||||
let achieved = missing_km == 0
|
||||
&& (multi_day_trips_over_required_distance.len() >= 1
|
||||
|| single_day_trips_over_required_distance.len() >= 2);
|
||||
|
||||
Some(Self {
|
||||
year,
|
||||
rowed_km,
|
||||
category,
|
||||
required_km,
|
||||
missing_km,
|
||||
multi_day_trips_over_required_distance,
|
||||
single_day_trips_over_required_distance,
|
||||
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
|
||||
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
|
||||
achieved,
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{logbook::Logbook, user::User};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Rower {
|
||||
pub logbook_id: i64,
|
||||
pub rower_id: i64,
|
||||
}
|
||||
|
||||
impl Rower {
|
||||
pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::for_log_tx(&mut tx, log).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn for_log_tx(db: &mut Transaction<'_, Sqlite>, log: &Logbook) -> Vec<User> {
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
",
|
||||
log.id
|
||||
)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
logbook_id: i64,
|
||||
rower_id: i64,
|
||||
) -> Result<(), String> {
|
||||
//TODO: Check if rower is allowed to row
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO rower(logbook_id, rower_id) VALUES (?,?);",
|
||||
logbook_id,
|
||||
rower_id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::Logbook;
|
||||
use crate::model::{rower::Rower, user::User};
|
||||
use crate::testdb;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_for_log() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 3).await.unwrap();
|
||||
let rowers = Rower::for_log(&pool, &logbook).await;
|
||||
let expected = User::find_by_id(&pool, 3).await.unwrap();
|
||||
assert_eq!(rowers, vec![expected]);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_for_log_none() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 2).await.unwrap();
|
||||
let rowers = Rower::for_log(&pool, &logbook).await;
|
||||
assert_eq!(rowers, vec![]);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 3).await.unwrap();
|
||||
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
Rower::create(&mut tx, logbook.id, 2).await.unwrap();
|
||||
tx.commit().await.unwrap();
|
||||
let rowers = Rower::for_log(&pool, &logbook).await;
|
||||
assert_eq!(
|
||||
rowers,
|
||||
vec![
|
||||
User::find_by_id(&pool, 2).await.unwrap(),
|
||||
User::find_by_id(&pool, 3).await.unwrap()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,296 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::user::User;
|
||||
use chrono::Datelike;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::boat::Boat;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct BoatStat {
|
||||
pot_years: Vec<i32>,
|
||||
boats: Vec<SingleBoatStat>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct SingleBoatStat {
|
||||
name: String,
|
||||
cat: String,
|
||||
location: String,
|
||||
owner: String,
|
||||
years: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl BoatStat {
|
||||
pub async fn get(db: &SqlitePool) -> BoatStat {
|
||||
let mut years = Vec::new();
|
||||
let mut boat_stats_map: HashMap<String, SingleBoatStat> = HashMap::new();
|
||||
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
boat.id,
|
||||
location.name AS location,
|
||||
CAST(strftime('%Y', COALESCE(arrival, 'now')) AS INTEGER) AS year,
|
||||
CAST(SUM(COALESCE(distance_in_km, 0)) AS INTEGER) AS rowed_km
|
||||
FROM
|
||||
boat
|
||||
LEFT JOIN
|
||||
logbook ON boat.id = logbook.boat_id AND logbook.arrival IS NOT NULL
|
||||
LEFT JOIN
|
||||
location ON boat.location_id = location.id
|
||||
WHERE
|
||||
not boat.external
|
||||
GROUP BY
|
||||
boat.id, year
|
||||
ORDER BY
|
||||
boat.name, year DESC;
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for row in rows {
|
||||
let id: i32 = row.get("id");
|
||||
let boat = Boat::find_by_id(db, id).await.unwrap();
|
||||
let owner = if let Some(owner) = boat.owner(db).await {
|
||||
owner.name
|
||||
} else {
|
||||
String::from("Verein")
|
||||
};
|
||||
let name = boat.name.clone();
|
||||
let location: String = row.get("location");
|
||||
let year: i32 = row.get("year");
|
||||
if year == 0 {
|
||||
continue; // Boat still on water
|
||||
}
|
||||
|
||||
if !years.contains(&year) {
|
||||
years.push(year);
|
||||
}
|
||||
|
||||
let year: String = format!("{year}");
|
||||
let cat = boat.cat();
|
||||
|
||||
let rowed_km: i32 = row.get("rowed_km");
|
||||
|
||||
let boat_stat = boat_stats_map
|
||||
.entry(name.clone())
|
||||
.or_insert(SingleBoatStat {
|
||||
name,
|
||||
location,
|
||||
owner,
|
||||
cat,
|
||||
years: HashMap::new(),
|
||||
});
|
||||
boat_stat.years.insert(year, rowed_km);
|
||||
}
|
||||
|
||||
BoatStat {
|
||||
pot_years: years,
|
||||
boats: boat_stats_map.into_values().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Stat {
|
||||
name: String,
|
||||
pub(crate) rowed_km: i32,
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let rowed_km = sqlx::query(&format!(
|
||||
"
|
||||
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
|
||||
FROM logbook l
|
||||
JOIN boat b ON l.boat_id = b.id
|
||||
LEFT JOIN (
|
||||
SELECT logbook_id, COUNT(*) as member_count
|
||||
FROM rower
|
||||
GROUP BY logbook_id
|
||||
) m ON l.id = m.logbook_id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external;
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
let rowed_km_guests = sqlx::query(&format!(
|
||||
"
|
||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE u.id NOT IN (
|
||||
SELECT ur.user_id
|
||||
FROM user_role ur
|
||||
INNER JOIN role ro ON ur.role_id = ro.id
|
||||
WHERE ro.name = 'Donau Linz'
|
||||
)
|
||||
AND l.distance_in_km IS NOT NULL
|
||||
AND l.arrival LIKE '{year}-%'
|
||||
AND u.name != 'Externe Steuerperson';
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
Stat {
|
||||
name: "Gäste".into(),
|
||||
rowed_km: rowed_km + rowed_km_guests,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
|
||||
let stats = Self::people(db, year).await;
|
||||
let mut sum = 0;
|
||||
for stat in stats {
|
||||
sum += stat.rowed_km;
|
||||
}
|
||||
|
||||
sum
|
||||
}
|
||||
|
||||
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id IN (
|
||||
SELECT user_id FROM user_role
|
||||
JOIN role ON user_role.role_id = role.id
|
||||
WHERE role.name = 'Donau Linz'
|
||||
)
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson'
|
||||
GROUP BY u.name
|
||||
ORDER BY rowed_km DESC, u.name;
|
||||
"
|
||||
))
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let row = sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL;
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let row = sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PersonalStat {
|
||||
date: String,
|
||||
km: i32,
|
||||
}
|
||||
|
||||
pub async fn get_personal(db: &SqlitePool, user: &User) -> Vec<PersonalStat> {
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT
|
||||
departure_date as date,
|
||||
SUM(total_distance) OVER (ORDER BY departure_date) as km
|
||||
FROM (
|
||||
SELECT
|
||||
date(l.departure) as departure_date,
|
||||
COALESCE(SUM(l.distance_in_km),0) as total_distance
|
||||
FROM
|
||||
logbook l
|
||||
LEFT JOIN
|
||||
rower r ON l.id = r.logbook_id
|
||||
WHERE
|
||||
r.rower_id = {}
|
||||
GROUP BY
|
||||
departure_date
|
||||
) as subquery
|
||||
ORDER BY
|
||||
departure_date;
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| PersonalStat {
|
||||
date: row.get("date"),
|
||||
km: row.get("km"),
|
||||
})
|
||||
.collect()
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Trailer {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Trailer {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
@@ -1,233 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
use super::trailer::Trailer;
|
||||
use super::user::User;
|
||||
use crate::tera::trailerreservation::ReservationEditForm;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservation {
|
||||
pub id: i64,
|
||||
pub trailer_id: i64,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: String,
|
||||
pub usage: String,
|
||||
pub user_id_applicant: i64,
|
||||
pub user_id_confirmation: Option<i64>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: TrailerReservation,
|
||||
trailer: Trailer,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TrailerReservationToAdd<'r> {
|
||||
pub trailer: &'r Trailer,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl TrailerReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<TrailerReservationWithDetails> {
|
||||
let trailerreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for reservation in trailerreservations {
|
||||
let user_confirmation = match reservation.user_id_confirmation {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailer = Trailer::find_by_id(db, reservation.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(TrailerReservationWithDetails {
|
||||
reservation,
|
||||
trailer,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<TrailerReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<TrailerReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
reservation.reservation.start_date,
|
||||
reservation.reservation.end_date,
|
||||
reservation.reservation.time_desc,
|
||||
reservation.reservation.usage,
|
||||
reservation.user_applicant.name
|
||||
);
|
||||
|
||||
grouped_reservations
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(reservation);
|
||||
}
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
trailerreservation: TrailerReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::trailer_reserved_between_dates(
|
||||
db,
|
||||
trailerreservation.trailer,
|
||||
&trailerreservation.start_date,
|
||||
&trailerreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Hänger in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("New trailer reservation: {trailerreservation:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO trailer_reservation(trailer_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
trailerreservation.trailer.id,
|
||||
trailerreservation.start_date,
|
||||
trailerreservation.end_date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage,
|
||||
trailerreservation.user_applicant.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let board =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
|
||||
for user in board {
|
||||
let date = if trailerreservation.start_date == trailerreservation.end_date {
|
||||
format!("am {}", trailerreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
trailerreservation.start_date, trailerreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Hängerreservierung für Hänger '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
trailerreservation.user_applicant.name,
|
||||
trailerreservation.trailer.name,
|
||||
date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage
|
||||
),
|
||||
"Neue Hängerreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn trailer_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
trailer: &Trailer,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM trailer_reservation
|
||||
WHERE trailer_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
trailer.id,
|
||||
end_date,
|
||||
start_date
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.reservation_count
|
||||
> 0
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
|
||||
let time_desc = data.time_desc.trim();
|
||||
let usage = data.usage.trim();
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET time_desc = ?, usage = ? where id = ?",
|
||||
time_desc,
|
||||
usage,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM trailer_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
@@ -9,7 +9,7 @@ use super::{
|
||||
notification::Notification,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{ErgoUser, SteeringUser, User},
|
||||
user::{SteeringUser, User},
|
||||
usertrip::UserTrip,
|
||||
};
|
||||
|
||||
@@ -66,16 +66,6 @@ impl Trip {
|
||||
Self::perform_new(db, &cox.user, trip_details).await
|
||||
}
|
||||
|
||||
pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) {
|
||||
let typ = trip_details.triptype(db).await;
|
||||
if let Some(typ) = typ {
|
||||
let allowed_type = TripType::find_by_id(db, 4).await.unwrap();
|
||||
if typ == allowed_type {
|
||||
Self::perform_new(db, &ergo.user, trip_details).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
|
||||
|
@@ -6,42 +6,24 @@ use log::info;
|
||||
use rocket::{
|
||||
async_trait,
|
||||
http::{Cookie, Status},
|
||||
request::{self, FromRequest, Outcome},
|
||||
request,
|
||||
request::{FromRequest, Outcome},
|
||||
time::{Duration, OffsetDateTime},
|
||||
tokio::io::AsyncReadExt,
|
||||
Request,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{
|
||||
family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role,
|
||||
stat::Stat, tripdetails::TripDetails, Day,
|
||||
};
|
||||
use crate::{
|
||||
tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE,
|
||||
EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG,
|
||||
SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND,
|
||||
};
|
||||
use super::{log::Log, role::Role, tripdetails::TripDetails, Day};
|
||||
use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub pw: Option<String>,
|
||||
pub dob: Option<String>,
|
||||
pub weight: Option<String>,
|
||||
pub sex: Option<String>,
|
||||
pub deleted: bool,
|
||||
pub last_access: Option<chrono::NaiveDateTime>,
|
||||
pub member_since_date: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
pub mail: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub address: Option<String>,
|
||||
pub family_id: Option<i64>,
|
||||
pub user_token: String,
|
||||
}
|
||||
|
||||
@@ -51,7 +33,6 @@ pub struct UserWithDetails {
|
||||
pub user: User,
|
||||
pub amount_unread_notifications: i32,
|
||||
pub allowed_to_steer: bool,
|
||||
pub on_water: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -60,7 +41,6 @@ impl UserWithDetails {
|
||||
let allowed_to_steer = user.allowed_to_steer(db).await;
|
||||
|
||||
Self {
|
||||
on_water: user.on_water(db).await,
|
||||
roles: user.roles(db).await,
|
||||
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||
allowed_to_steer,
|
||||
@@ -83,62 +63,6 @@ pub enum LoginError {
|
||||
DeserializationError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Fee {
|
||||
pub sum_in_cents: i32,
|
||||
pub parts: Vec<(String, i32)>,
|
||||
pub name: String,
|
||||
pub user_ids: String,
|
||||
pub paid: bool,
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
impl Default for Fee {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Fee {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sum_in_cents: 0,
|
||||
name: "".into(),
|
||||
parts: Vec::new(),
|
||||
user_ids: "".into(),
|
||||
users: Vec::new(),
|
||||
paid: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, desc: String, price_in_cents: i32) {
|
||||
self.sum_in_cents += price_in_cents;
|
||||
|
||||
self.parts.push((desc, price_in_cents));
|
||||
}
|
||||
|
||||
pub fn add_person(&mut self, user: &User) {
|
||||
if !self.name.is_empty() {
|
||||
self.name.push_str(" + ");
|
||||
self.user_ids.push('&');
|
||||
}
|
||||
self.name.push_str(&user.name);
|
||||
|
||||
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
|
||||
self.users.push(user.clone());
|
||||
}
|
||||
|
||||
pub fn paid(&mut self) {
|
||||
self.paid = true;
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, fee: Fee) {
|
||||
for (desc, price_in_cents) in fee.parts {
|
||||
self.add(desc, price_in_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool {
|
||||
self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await
|
||||
@@ -148,315 +72,6 @@ impl User {
|
||||
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
|
||||
}
|
||||
|
||||
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Could not send welcome mail, because user {} has no email address",
|
||||
self.name
|
||||
));
|
||||
};
|
||||
|
||||
if self.has_role(db, "Donau Linz").await {
|
||||
self.send_welcome_mail_full_member(db, mail, smtp_pw)
|
||||
.await?;
|
||||
} else if self.has_role(db, "scheckbuch").await {
|
||||
self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await?;
|
||||
} else if self.has_role(db, "schnupperant").await {
|
||||
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
|
||||
self.name
|
||||
));
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("Willkommensemail wurde an {} versandt", self.name),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_schnupper(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
// 2 things to do:
|
||||
// 1. Send mail to user
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Schnupperrudern beim ASKÖ Ruderverein Donau Linz",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
|
||||
|
||||
Liebe Grüße, Philipp", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&coxes,
|
||||
&format!(
|
||||
"Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.",
|
||||
self.name
|
||||
),
|
||||
"Neue(r) Schnupperteilnehmer:in ",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_scheckbuch(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
// 2 things to do:
|
||||
// 1. Send mail to user
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]).
|
||||
|
||||
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-)
|
||||
|
||||
Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||
self.name
|
||||
),
|
||||
"Neues Scheckbuch",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_end_mail_scheckbuch(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
Mail::send_single_tx(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Deine Mitgliedschaft wartet auf Dich",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlichen Glückwunsch---Du hast Deine fünf Scheckbuch-Ausfahrten erfolgreich absolviert! Wir hoffen, dass Du das Rudern bei uns genauso genossen hast wie wir es genossen haben, Dich auf dem Wasser zu begleiten.
|
||||
|
||||
Wir würden uns sehr freuen, Dich als festes Mitglied in unserem Verein willkommen zu heißen! Als Mitglied stehen Dir dann alle unsere Ausfahrten offen, die von unseren Steuerleuten organisiert werden. Im Sommer erwarten Dich zusätzlich spannende Events: Wanderfahrten, Sternfahrten, Fetzenfahrt, .... Im Winter bieten wir Indoor-Ergo-Challenges an, bei denen Du Deine Fitness auf dem Ruderergometer unter Beweis stellen kannst. Alle Details zu diesen Aktionen erfährst Du, sobald Du Teil unseres Vereins bist! :-)
|
||||
|
||||
Alle Informationen zu den Mitgliedsbeiträgen findest Du unter https://rudernlinz.at/unser-verein/gebuhren/ Falls Du Dich entscheidest, unserem Verein beizutreten, fülle bitte unser Beitrittsformular auf https://rudernlinz.at/unser-verein/downloads/ aus und sende es an info@rudernlinz.at.
|
||||
|
||||
Wir freuen uns, Dich bald wieder auf dem Wasser zu sehen.
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_full_member(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
// 2 things to do:
|
||||
// 1. Send mail to user
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Willkommen im ASKÖ Ruderverein Donau Linz!",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
|
||||
|
||||
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
|
||||
|
||||
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
|
||||
|
||||
Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernlinz.at/unser-verein/vorstand/) bitte daran, deinen Fingerabdruck zu registrieren, damit du Zugang zum Bootshaus erhältst.
|
||||
|
||||
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
|
||||
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||
|
||||
Riemen- & Dollenbruch
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
|
||||
self.member_since_date.clone().unwrap(),
|
||||
self.name
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
||||
if !self.has_role(db, "Donau Linz").await {
|
||||
return None;
|
||||
}
|
||||
if self.deleted {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut fee = Fee::new();
|
||||
|
||||
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
|
||||
for member in family.members(db).await {
|
||||
fee.add_person(&member);
|
||||
if member.has_role(db, "paid").await {
|
||||
fee.paid();
|
||||
}
|
||||
fee.merge(member.fee_without_families(db).await);
|
||||
}
|
||||
if family.amount_family_members(db).await > 2 {
|
||||
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
|
||||
} else {
|
||||
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
|
||||
}
|
||||
} else {
|
||||
fee.add_person(self);
|
||||
if self.has_role(db, "paid").await {
|
||||
fee.paid();
|
||||
}
|
||||
fee.merge(self.fee_without_families(db).await);
|
||||
}
|
||||
|
||||
Some(fee)
|
||||
}
|
||||
|
||||
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
|
||||
let mut fee = Fee::new();
|
||||
|
||||
if !self.has_role(db, "Donau Linz").await {
|
||||
return fee;
|
||||
}
|
||||
if self.has_role(db, "Rennrudern").await {
|
||||
if self.has_role(db, "half-rennrudern").await {
|
||||
fee.add("Rennruderbeitrag (1/2 Preis) ".into(), RENNRUDERBEITRAG / 2);
|
||||
} else {
|
||||
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
|
||||
}
|
||||
}
|
||||
|
||||
let amount_boats = self.amount_boats(db).await;
|
||||
if amount_boats > 0 {
|
||||
fee.add(
|
||||
format!("{}x Bootsplatz", amount_boats),
|
||||
amount_boats * BOAT_STORAGE,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let halfprice = 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();
|
||||
member_since_date >= halfprice_startdate
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if self.has_role(db, "Unterstützend").await {
|
||||
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
|
||||
} else if self.has_role(db, "Förderndes Mitglied").await {
|
||||
fee.add("Förderndes Mitglied".into(), FOERDERND);
|
||||
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
|
||||
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
||||
if halfprice {
|
||||
fee.add("Schüler/Student (Halbpreis)".into(), STUDENT_OR_PUPIL / 2);
|
||||
} else {
|
||||
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
|
||||
}
|
||||
} else if self.has_role(db, "Ehrenmitglied").await {
|
||||
fee.add("Ehrenmitglied".into(), 0);
|
||||
} else {
|
||||
if halfprice {
|
||||
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
|
||||
} else {
|
||||
fee.add("Mitgliedsbeitrag".into(), REGULAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fee
|
||||
}
|
||||
|
||||
pub async fn amount_boats(&self, db: &SqlitePool) -> i32 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.count
|
||||
}
|
||||
|
||||
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i32 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
|
||||
@@ -491,29 +106,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool {
|
||||
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
Some(a) if a.is_empty() => false,
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
pub async fn has_membership_pdf_tx(&self, db: &mut Transaction<'_, Sqlite>) -> bool {
|
||||
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
Some(a) if a.is_empty() => false,
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
||||
sqlx::query!(
|
||||
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
|
||||
@@ -561,7 +153,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
@@ -576,7 +168,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
@@ -593,7 +185,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE lower(name)=?
|
||||
",
|
||||
@@ -604,38 +196,11 @@ WHERE lower(name)=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn on_water(&self, db: &SqlitePool) -> bool {
|
||||
if sqlx::query!(
|
||||
"SELECT * FROM logbook WHERE shipmaster=? AND arrival is null",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if sqlx::query!(
|
||||
"SELECT * FROM logbook JOIN rower ON rower.logbook_id=logbook.id WHERE rower_id=? AND arrival is null",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0
|
||||
ORDER BY last_access DESC
|
||||
@@ -657,48 +222,24 @@ ORDER BY last_access DESC
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user u
|
||||
JOIN user_role ur ON u.id = ur.user_id
|
||||
WHERE ur.role_id = ? AND deleted = 0
|
||||
ORDER BY name;
|
||||
", role.id
|
||||
",
|
||||
role.id
|
||||
)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn all_payer_groups(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user
|
||||
WHERE family_id IS NOT NULL
|
||||
GROUP BY family_id
|
||||
|
||||
UNION
|
||||
|
||||
-- Select users with a null family_id, without grouping
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user
|
||||
WHERE family_id IS NULL;
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn ergo(db: &SqlitePool) -> Vec<Self> {
|
||||
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
|
||||
Self::all_with_role(db, &ergo).await
|
||||
}
|
||||
|
||||
pub async fn cox(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
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
|
||||
@@ -717,70 +258,13 @@ ORDER BY last_access DESC
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn create_with_mail(db: &SqlitePool, name: &str, mail: &str) -> bool {
|
||||
let name = name.trim();
|
||||
sqlx::query!("INSERT INTO USER(name, mail) VALUES (?, ?)", name, mail)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
|
||||
dob,
|
||||
weight,
|
||||
sex,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) -> Result<(), String> {
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) -> Result<(), String> {
|
||||
let mut db = db.begin().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut family_id = data.family_id;
|
||||
|
||||
if family_id.is_some_and(|x| x == -1) {
|
||||
family_id = Some(Family::insert_tx(&mut db).await)
|
||||
}
|
||||
|
||||
if !self.has_membership_pdf_tx(&mut db).await {
|
||||
if let Some(membership_pdf) = data.membership_pdf {
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
let mut buffer = Vec::new();
|
||||
stream.read_to_end(&mut buffer).await.unwrap();
|
||||
sqlx::query!(
|
||||
"UPDATE user SET membership_pdf = ? where id = ?",
|
||||
buffer,
|
||||
self.id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
|
||||
data.dob,
|
||||
data.weight,
|
||||
data.sex,
|
||||
data.member_since_date,
|
||||
data.birthdate,
|
||||
data.mail,
|
||||
data.nickname,
|
||||
data.notes,
|
||||
data.phone,
|
||||
data.address,
|
||||
family_id,
|
||||
self.id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
sqlx::query!("UPDATE user SET name = ? where id = ?", data.name, self.id)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
// handle roles
|
||||
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
|
||||
@@ -858,41 +342,7 @@ ORDER BY last_access DESC
|
||||
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
||||
let name = name.trim().to_lowercase(); // just to make sure...
|
||||
let Some(user) = User::find_by_name(db, &name).await else {
|
||||
if ![
|
||||
"n-sageder",
|
||||
"p-hofer",
|
||||
"marie-birner",
|
||||
"daniel-kortschak",
|
||||
"rudernlinz",
|
||||
"m-birner",
|
||||
"s-sollberger",
|
||||
"d-kortschak",
|
||||
"wwwadmin",
|
||||
"wadminw",
|
||||
"admin",
|
||||
"m sageder",
|
||||
"d kortschak",
|
||||
"a almousa",
|
||||
"p hofer",
|
||||
"s sollberger",
|
||||
"n sageder",
|
||||
"wp-system",
|
||||
"s.sollberger",
|
||||
"m.birner",
|
||||
"m-sageder",
|
||||
"a-almousa",
|
||||
"m.sageder",
|
||||
"n.sageder",
|
||||
"a.almousa",
|
||||
"p.hofer",
|
||||
"philipp-hofer",
|
||||
"d.kortschak",
|
||||
"[login]",
|
||||
]
|
||||
.contains(&name.as_str())
|
||||
{
|
||||
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
||||
}
|
||||
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
||||
return Err(LoginError::InvalidAuthenticationCombo); // Username not found
|
||||
};
|
||||
|
||||
@@ -1013,60 +463,6 @@ ORDER BY last_access DESC
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn close_thousands_trip(&self, db: &SqlitePool) -> Option<String> {
|
||||
let rowed_km = Stat::person(db, None, self).await.rowed_km;
|
||||
if rowed_km % 1000 > 970 {
|
||||
return Some(format!(
|
||||
"{} braucht nur mehr {} km bis die {} km voll sind 🤑",
|
||||
self.name,
|
||||
1000 - rowed_km % 1000,
|
||||
rowed_km + 1000 - (rowed_km % 1000)
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn received_new_logentry(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
smtp_pw: &str,
|
||||
) {
|
||||
if self.has_role_tx(db, "scheckbuch").await {
|
||||
let amount_trips = Logbook::completed_with_user_tx(db, &self).await.len();
|
||||
if amount_trips == 5 {
|
||||
if let Some(mail) = &self.mail {
|
||||
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
|
||||
}
|
||||
Notification::create_for_steering_people_tx(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
|
||||
self.name
|
||||
),
|
||||
"Scheckbuch fertig",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
} else if amount_trips > 5 {
|
||||
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&board,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.",
|
||||
self.name, amount_trips
|
||||
),
|
||||
"Scheckbuch überfertig",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check fahrtenabzeichen fertig?
|
||||
// TODO: check äquatorpreis geschafft?
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -1165,62 +561,12 @@ macro_rules! special_user {
|
||||
};
|
||||
}
|
||||
|
||||
special_user!(TechUser, +"tech");
|
||||
special_user!(ErgoUser, +"ergo");
|
||||
special_user!(SteeringUser, +"cox", +"Bootsführer");
|
||||
special_user!(SteeringUser, +"cox");
|
||||
special_user!(AdminUser, +"admin");
|
||||
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch");
|
||||
special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied");
|
||||
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
|
||||
special_user!(VorstandUser, +"Vorstand");
|
||||
special_user!(EventUser, +"manage_events");
|
||||
special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
|
||||
special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
|
||||
special_user!(ManageUserUser, +"admin");
|
||||
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserWithRolesAndMembershipPdf {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub membership_pdf: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
impl UserWithRolesAndMembershipPdf {
|
||||
pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self {
|
||||
let membership_pdf = user.has_membership_pdf(db).await;
|
||||
|
||||
Self {
|
||||
roles: user.roles(db).await,
|
||||
user,
|
||||
membership_pdf,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserWithMembershipPdf {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub membership_pdf: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl UserWithMembershipPdf {
|
||||
pub(crate) async fn from(db: &SqlitePool, user: User) -> Self {
|
||||
let membership_pdf: Option<Vec<u8>> =
|
||||
sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = $1", user.id)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
user,
|
||||
membership_pdf,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
Reference in New Issue
Block a user