Merge pull request 'final' (#168) from final into staging
Reviewed-on: #168
This commit is contained in:
commit
519cd1985d
@ -1,6 +1,8 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
|
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
|
||||||
|
|
||||||
|
use super::user::User;
|
||||||
|
|
||||||
#[derive(FromRow, Serialize, Clone)]
|
#[derive(FromRow, Serialize, Clone)]
|
||||||
pub struct Family {
|
pub struct Family {
|
||||||
id: i64,
|
id: i64,
|
||||||
@ -46,10 +48,36 @@ GROUP BY family.id;"
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||||
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
|
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
|
||||||
.fetch_one(db)
|
.fetch_one(db)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.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 FROM user WHERE family_id = ?", self.id)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut};
|
|||||||
|
|
||||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||||
use chrono::{Datelike, Local, NaiveDate};
|
use chrono::{Datelike, Local, NaiveDate};
|
||||||
|
use chrono_tz::Etc::UTC;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
async_trait,
|
async_trait,
|
||||||
@ -16,6 +17,15 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
|||||||
use super::{family::Family, log::Log, tripdetails::TripDetails, Day};
|
use super::{family::Family, log::Log, tripdetails::TripDetails, Day};
|
||||||
use crate::tera::admin::user::UserEditForm;
|
use crate::tera::admin::user::UserEditForm;
|
||||||
|
|
||||||
|
const RENNRUDERBEITRAG: i32 = 11000;
|
||||||
|
const BOAT_STORAGE: i32 = 4500;
|
||||||
|
const FAMILY_TWO: i32 = 30000;
|
||||||
|
const FAMILY_THREE_OR_MORE: i32 = 35000;
|
||||||
|
const STUDENT_OR_PUPIL: i32 = 8000;
|
||||||
|
const REGULAR: i32 = 22000;
|
||||||
|
const UNTERSTUETZEND: i32 = 2500;
|
||||||
|
const FOERDERND: i32 = 8500;
|
||||||
|
|
||||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
@ -90,7 +100,109 @@ pub enum LoginError {
|
|||||||
DeserializationError,
|
DeserializationError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub(crate) struct Fee {
|
||||||
|
pub(crate) sum_in_cents: i32,
|
||||||
|
pub(crate) parts: Vec<(String, i32)>,
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fee {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sum_in_cents: 0,
|
||||||
|
name: "".into(),
|
||||||
|
parts: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 name(&mut self, name: String) {
|
||||||
|
self.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, fee: Fee) {
|
||||||
|
for (desc, price_in_cents) in fee.parts {
|
||||||
|
self.add(desc, price_in_cents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
||||||
|
if !self.has_role(db, "Donau Linz").await {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fee = Fee::new();
|
||||||
|
|
||||||
|
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
|
||||||
|
fee.name(format!("{} + Familie", self.name));
|
||||||
|
for member in family.members(db).await {
|
||||||
|
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.name(self.name.clone());
|
||||||
|
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 {
|
||||||
|
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 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 {
|
||||||
|
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
|
||||||
|
} 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 rowed_km(&self, db: &SqlitePool) -> i32 {
|
pub async fn rowed_km(&self, db: &SqlitePool) -> i32 {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"SELECT COALESCE(SUM(distance_in_km),0) as rowed_km
|
"SELECT COALESCE(SUM(distance_in_km),0) as rowed_km
|
||||||
@ -640,6 +752,42 @@ impl<'r> FromRequest<'r> for DonauLinzUser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct VorstandUser(pub(crate) User);
|
||||||
|
|
||||||
|
impl Into<User> for VorstandUser {
|
||||||
|
fn into(self) -> User {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for VorstandUser {
|
||||||
|
type Target = User;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for VorstandUser {
|
||||||
|
type Error = LoginError;
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||||
|
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||||
|
match User::from_request(req).await {
|
||||||
|
Outcome::Success(user) => {
|
||||||
|
if user.has_role(db, "Vorstand").await {
|
||||||
|
Outcome::Success(VorstandUser(user))
|
||||||
|
} else {
|
||||||
|
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Outcome::Error(f) => Outcome::Error(f),
|
||||||
|
Outcome::Forward(f) => Outcome::Forward(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -3,9 +3,9 @@ use std::collections::HashMap;
|
|||||||
use crate::model::{
|
use crate::model::{
|
||||||
family::Family,
|
family::Family,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{AdminUser, User, UserWithRoles},
|
user::{AdminUser, Fee, User, UserWithRoles, VorstandUser},
|
||||||
};
|
};
|
||||||
use futures::future::join_all;
|
use futures::future::{self, join_all};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
get, post,
|
get, post,
|
||||||
@ -51,40 +51,27 @@ async fn index(
|
|||||||
#[get("/user/fees")]
|
#[get("/user/fees")]
|
||||||
async fn fees(
|
async fn fees(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
admin: AdminUser,
|
admin: VorstandUser,
|
||||||
flash: Option<FlashMessage<'_>>,
|
flash: Option<FlashMessage<'_>>,
|
||||||
) -> Template {
|
) -> Template {
|
||||||
// Checks
|
|
||||||
// Fördernd -> Donau Linz
|
|
||||||
// Fördernd -> keine Family
|
|
||||||
// Unterstützend -> Donau Linz
|
|
||||||
// Unterstützend -> keine Family
|
|
||||||
// Schüler -> Donau Linz
|
|
||||||
// Student -> Donau Linz
|
|
||||||
|
|
||||||
// Fördernd + boat: 85€
|
|
||||||
// Unterstützend + boat: 25€
|
|
||||||
|
|
||||||
// select id, size_of_family, count_rennsport, amount_boats from family
|
|
||||||
// 2-Family: 220€
|
|
||||||
// 3+-Family: 350€
|
|
||||||
|
|
||||||
// select student+schüler, amount_boats, rennsportbeitrag where !family
|
|
||||||
// Student, Schüler: 80€
|
|
||||||
|
|
||||||
// select donaulinz, amount_boats, rennsportbeitrag where !family !student ! schüler
|
|
||||||
// Normal: 220€
|
|
||||||
|
|
||||||
// Rennsportbeitrag: 110€
|
|
||||||
// Bootsplatz: 45€
|
|
||||||
|
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
|
|
||||||
|
let users = User::all(db).await;
|
||||||
|
let mut fees = Vec::new();
|
||||||
|
for user in users {
|
||||||
|
if let Some(fee) = user.fee(db).await {
|
||||||
|
fees.push(fee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.insert("fees", &fees);
|
||||||
|
|
||||||
if let Some(msg) = flash {
|
if let Some(msg) = flash {
|
||||||
context.insert("flash", &msg.into_inner());
|
context.insert("flash", &msg.into_inner());
|
||||||
}
|
}
|
||||||
context.insert(
|
context.insert(
|
||||||
"loggedin_user",
|
"loggedin_user",
|
||||||
&UserWithRoles::from_user(admin.user, db).await,
|
&UserWithRoles::from_user(admin.into(), db).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
Template::render("admin/user/fees", context.into_json())
|
Template::render("admin/user/fees", context.into_json())
|
||||||
|
32
templates/admin/user/fees.html.tera
Normal file
32
templates/admin/user/fees.html.tera
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% import "includes/macros" as macros %}
|
||||||
|
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="max-w-screen-lg w-full">
|
||||||
|
<h1 class="h1">Ergo Challenges</h1>
|
||||||
|
|
||||||
|
{% if flash %}
|
||||||
|
{{ macros::alert(message=flash.1, type=flash.0, class="my-3") }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="grid gap-3">
|
||||||
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
|
||||||
|
<h2 class="h2">Gebühren</h2>
|
||||||
|
<div class="text-sm p-3">
|
||||||
|
{% for fee in fees | sort(attribute="name") %}
|
||||||
|
<b>{{ fee.name }}: {{ fee.sum_in_cents / 100 }}€</b><br />
|
||||||
|
{% for p in fee.parts %}
|
||||||
|
{{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<hr />
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content%}
|
||||||
|
|
@ -4,13 +4,17 @@
|
|||||||
>
|
>
|
||||||
<div class="max-w-screen-xl w-full flex justify-between items-center">
|
<div class="max-w-screen-xl w-full flex justify-between items-center">
|
||||||
<div class="w-1/3 truncate">
|
<div class="w-1/3 truncate">
|
||||||
<a href="/">
|
{% if "Donau Linz" in loggedin_user.roles %}
|
||||||
|
<a href="/planned">
|
||||||
|
{% else %}
|
||||||
|
<a href="/">
|
||||||
|
{% endif %}
|
||||||
Hü
|
Hü
|
||||||
{{ loggedin_user.name }}
|
{{ loggedin_user.name }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div><!--
|
<div>
|
||||||
<a
|
<a
|
||||||
href="https://wiki.rudernlinz.at/ruderassistent#faq"
|
href="https://wiki.rudernlinz.at/ruderassistent#faq"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@ -48,7 +52,7 @@
|
|||||||
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
||||||
data-sidebar="true"
|
data-sidebar="true"
|
||||||
data-trigger="sidebar"
|
data-trigger="sidebar"
|
||||||
data-header="Logbuch"
|
data-header="Menü"
|
||||||
data-body="#mobile-menu"
|
data-body="#mobile-menu"
|
||||||
>
|
>
|
||||||
{% include "includes/book" %}
|
{% include "includes/book" %}
|
||||||
@ -121,7 +125,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="sr-only">Userverwaltung</span>
|
<span class="sr-only">Userverwaltung</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}-->
|
{% endif %}
|
||||||
<a
|
<a
|
||||||
href="/auth/logout"
|
href="/auth/logout"
|
||||||
class="inline-flex justify-center rounded-md bg-gray-200 ml-1 px-3 py-2 text-sm font-semibold text-primary-950 hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
class="inline-flex justify-center rounded-md bg-gray-200 ml-1 px-3 py-2 text-sm font-semibold text-primary-950 hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
||||||
|
@ -66,6 +66,19 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if "Vorstand" in loggedin_user.roles %}
|
||||||
|
<div class="grid gap-3">
|
||||||
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
|
||||||
|
<h2 class="h2">Vorstand</h2>
|
||||||
|
<div class="text-sm p-3">
|
||||||
|
<ul class="list-disc ms-2">
|
||||||
|
<li class="py-1"><a href="/admin/user/fees" class="link-primary">Übersicht User Gebühren</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if "admin" in loggedin_user.roles %}
|
{% if "admin" in loggedin_user.roles %}
|
||||||
<div class="grid gap-3">
|
<div class="grid gap-3">
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
|
||||||
|
Loading…
Reference in New Issue
Block a user