be able to update data individually; Fixes #952
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped

This commit is contained in:
Philipp Hofer 2025-04-30 13:38:45 +02:00
parent c8d5c633d7
commit d2914f9287
6 changed files with 392 additions and 11 deletions

View File

@ -7,7 +7,7 @@ use super::user::User;
#[derive(FromRow, Serialize, Clone)]
pub struct Family {
id: i64,
pub(crate) id: i64,
}
#[derive(Serialize, Clone)]
@ -91,4 +91,18 @@ GROUP BY family.id;"
.await
.unwrap()
}
pub async fn clean_families_without_members(db: &SqlitePool) {
sqlx::query(
"DELETE FROM family
WHERE id NOT IN (
SELECT DISTINCT family_id
FROM user
WHERE family_id IS NOT NULL
);",
)
.execute(db)
.await
.unwrap();
}
}

View File

@ -47,7 +47,7 @@ pub struct TripUpdate<'a> {
pub is_locked: bool,
}
impl<'a> TripUpdate<'a> {
impl TripUpdate<'_> {
fn cancelled(&self) -> bool {
self.max_people == -1
}

View File

@ -1,7 +1,9 @@
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
use crate::model::{log::Log, mail::valid_mails, role::Role};
use crate::model::{family::Family, log::Log, mail::valid_mails, role::Role};
use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool;
impl User {
@ -60,6 +62,35 @@ impl User {
Ok(())
}
pub(crate) async fn update_address(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_address: &str,
) -> Result<(), String> {
let new_address = new_address.trim();
let query = if new_address.is_empty() {
sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id)
} else {
sqlx::query!(
"UPDATE user SET address = ? where id = ?",
new_address,
self.id
)
};
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.address {
Some(old_address) if new_address.is_empty() => format!("{updated_by} has removed the address of {self} (old address: {old_address})"),
Some(old_address) => format!("{updated_by} has changed the address of {self} from {old_address} to {new_address}"),
None => format!("{updated_by} has added an address for {self}: {new_address}")
};
Log::create(db, msg).await;
Ok(())
}
pub(crate) async fn update_nickname(
&self,
db: &SqlitePool,
@ -89,6 +120,82 @@ impl User {
Ok(())
}
pub(crate) async fn update_member_since(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_member_since_date: &NaiveDate,
) {
sqlx::query!(
"UPDATE user SET member_since_date = ? where id = ?",
new_member_since_date,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.member_since_date {
Some(old_member_since_date) => format!("{updated_by} has changed the member_since date of {self} from {old_member_since_date} to {new_member_since_date}"),
None => format!("{updated_by} has added a member_since_date for {self}: {new_member_since_date}")
};
Log::create(db, msg).await;
}
pub(crate) async fn update_birthdate(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_birthdate: &NaiveDate,
) {
sqlx::query!(
"UPDATE user SET birthdate = ? where id = ?",
new_birthdate,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.birthdate{
Some(old_birthdate) => format!("{updated_by} has changed the birthdate of {self} from {old_birthdate} to {new_birthdate}"),
None => format!("{updated_by} has added a birthdate for {self}: {new_birthdate}")
};
Log::create(db, msg).await;
}
pub(crate) async fn update_family(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
family: Option<Family>,
) {
if let Some(family) = family {
let family_id = family.id;
sqlx::query!(
"UPDATE user SET family_id = ? where id = ?",
family_id,
self.id
)
.execute(db)
.await
.unwrap();
} else {
sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id)
.execute(db)
.await
.unwrap();
};
Family::clean_families_without_members(db).await;
Log::create(
db,
format!("{updated_by} hat die Familie von {self} aktualisiert."),
)
.await;
}
pub(crate) async fn remove_role(
&self,
db: &SqlitePool,
@ -196,4 +303,35 @@ impl User {
Ok(())
}
pub(crate) async fn add_membership_pdf(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
if self.has_membership_pdf(db).await {
return Err(format!("User {self} hat bereits eine Beitrittserklärung."));
}
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)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
Log::create(
db,
format!("{updated_by} has added the membership pdf for user {self}"),
)
.await;
Ok(())
}
}

View File

@ -123,7 +123,7 @@ impl User {
.await?;
} else if self.has_role(db, "schnupperant").await {
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
} else if let Some(scheckbuch) = ScheckbuchUser::new(db, &self).await {
} else if let Some(scheckbuch) = ScheckbuchUser::new(db, self).await {
scheckbuch.notify(db, mail, smtp_pw).await?;
} else {
return Err(format!(
@ -272,7 +272,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
}
pub async fn allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool {
AllowedToUpdateTripToAlwaysBeShownUser::new(db, &self)
AllowedToUpdateTripToAlwaysBeShownUser::new(db, self)
.await
.is_some()
}

View File

@ -13,6 +13,7 @@ use crate::{
},
tera::Config,
};
use chrono::NaiveDate;
use futures::future::join_all;
use rocket::{
form::Form,
@ -389,6 +390,110 @@ async fn update_phone(
}
}
#[derive(FromForm, Debug)]
pub struct AddressUpdateForm {
address: String,
}
#[post("/user/<id>/change-address", data = "<data>")]
async fn update_address(
db: &State<SqlitePool>,
data: Form<AddressUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user.update_address(db, &admin, &data.address).await {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Adresse erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct FamilyUpdateForm {
family_id: Option<i64>,
}
#[post("/user/<id>/change-family", data = "<data>")]
async fn update_family(
db: &State<SqlitePool>,
data: Form<FamilyUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let family = match data.family_id {
Some(-1) => Some(
Family::find_by_id(db, Family::insert(db).await)
.await
.unwrap(),
),
Some(id) => match Family::find_by_id(db, id).await {
Some(f) => Some(f),
None => {
return Flash::error(
Redirect::to("/admin/user/{id}"),
format!("Family with ID {} does not exist!", id),
);
}
},
None => None,
};
user.update_family(db, &admin, family).await;
Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Familie erfolgreich geändert",
)
}
#[derive(FromForm, Debug)]
pub struct AddMembershipPDFForm<'a> {
membership_pdf: TempFile<'a>,
}
#[post("/user/<id>/add-membership-pdf", data = "<data>")]
async fn add_membership_pdf(
db: &State<SqlitePool>,
data: Form<AddMembershipPDFForm<'_>>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
match user
.add_membership_pdf(db, &admin, &data.membership_pdf)
.await
{
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Beitrittserklärung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct NicknameUpdateForm {
nickname: String,
@ -417,6 +522,77 @@ async fn update_nickname(
}
}
#[derive(FromForm, Debug)]
pub struct MemberSinceUpdateForm {
member_since: String,
}
#[post("/user/<id>/change-member-since", data = "<data>")]
async fn update_member_since(
db: &State<SqlitePool>,
data: Form<MemberSinceUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Ok(new_member_since_date) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d")
else {
return Flash::error(
Redirect::to("/admin/user/{id}"),
format!(
"Datum {} ist nicht im YYYY-MM-DD Format",
&data.member_since
),
);
};
user.update_member_since(db, &admin, &new_member_since_date)
.await;
Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Beitrittsdatum erfolgreich geändert",
)
}
#[derive(FromForm, Debug)]
pub struct BirthdateUpdateForm {
birthdate: String,
}
#[post("/user/<id>/change-birthdate", data = "<data>")]
async fn update_birthdate(
db: &State<SqlitePool>,
data: Form<BirthdateUpdateForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let Ok(new_birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
return Flash::error(
Redirect::to("/admin/user/{id}"),
format!("Datum {} ist nicht im YYYY-MM-DD Format", &data.birthdate),
);
};
user.update_birthdate(db, &admin, &new_birthdate).await;
Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Geburtstag erfolgreich geändert",
)
}
#[derive(FromForm, Debug)]
pub struct AddRoleForm {
role_id: i32,
@ -649,6 +825,11 @@ pub fn routes() -> Vec<Route> {
update_mail,
update_phone,
update_nickname,
update_member_since,
update_birthdate,
update_address,
update_family,
add_membership_pdf,
add_role,
remove_role,
]

View File

@ -41,7 +41,8 @@
</details>
{% endif %}
</li>
<li>Spitzname: {{ user.nickname }}
<li>
Spitzname: {{ user.nickname }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
@ -76,7 +77,9 @@
<fieldset>
<select name="role_id">
{% for role in roles %}
{% if not role.cluster and role not in user.proper_roles %}<option value="{{ role.id }}">{{ role.name }}</option>{% endif %}
{% if not role.cluster and role not in user.proper_roles %}
<option value="{{ role.id }}">{{ role.name }}</option>
{% endif %}
{% endfor %}
</select>
<input value="Rolle hinzufügen" type="submit" class="btn btn-primary ml-1" />
@ -93,11 +96,56 @@
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
<ul class="list-disc ms-4">
<li>Mitglied seit: {{ user.member_since_date }}</li>
<li>Geburtsdatum: {{ user.birthdate }}</li>
<li>Adresse: {{ user.address }}</li>
<li>
Familie: {{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
Mitglied seit: {{ user.member_since_date }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-member-since" method="post">
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=user.member_since_date) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Geburtsdatum: {{ user.birthdate }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-birthdate" method="post">
{{ macros::input(label='Geburtstag', name='birthdate', type="date", value=user.birthdate) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Adresse: {{ user.address }}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-address" method="post">
{{ macros::input(label='Neue Adresse', name='address', type="text", value=user.address) }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
<li>
Familie:
{% for family in families %}
{% if user.family_id == family.id %}{{ family.names }}{% endif %}
{% endfor %}
{% if allowed_to_edit %}
<details>
<summary>✏️</summary>
<form action="/admin/user/{{ user.id }}/change-family" method="post">
{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
<input value="Ändern" type="submit" class="btn btn-primary ml-1" />
</form>
</details>
{% endif %}
</li>
</ul>
</div>