notification #291
@ -16,7 +16,8 @@ CREATE TABLE IF NOT EXISTS "user" (
|
|||||||
"notes" text,
|
"notes" text,
|
||||||
"phone" text,
|
"phone" text,
|
||||||
"address" text,
|
"address" text,
|
||||||
"family_id" INTEGER REFERENCES family(id)
|
"family_id" INTEGER REFERENCES family(id),
|
||||||
|
"membership_pdf" BLOB
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "family" (
|
CREATE TABLE IF NOT EXISTS "family" (
|
||||||
|
@ -75,7 +75,7 @@ GROUP BY family.id;"
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
|
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)
|
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, membership_pdf FROM user WHERE family_id = ?", self.id)
|
||||||
.fetch_all(db)
|
.fetch_all(db)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -16,7 +16,7 @@ impl Rower {
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
User,
|
User,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user
|
FROM user
|
||||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||||
",
|
",
|
||||||
|
@ -8,6 +8,7 @@ use rocket::{
|
|||||||
http::{Cookie, Status},
|
http::{Cookie, Status},
|
||||||
request::{self, FromRequest, Outcome},
|
request::{self, FromRequest, Outcome},
|
||||||
time::{Duration, OffsetDateTime},
|
time::{Duration, OffsetDateTime},
|
||||||
|
tokio::io::AsyncReadExt,
|
||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -43,6 +44,7 @@ pub struct User {
|
|||||||
pub phone: Option<String>,
|
pub phone: Option<String>,
|
||||||
pub address: Option<String>,
|
pub address: Option<String>,
|
||||||
pub family_id: Option<i64>,
|
pub family_id: Option<i64>,
|
||||||
|
pub membership_pdf: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@ -285,7 +287,7 @@ impl User {
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user
|
FROM user
|
||||||
WHERE id like ?
|
WHERE id like ?
|
||||||
",
|
",
|
||||||
@ -300,7 +302,7 @@ WHERE id like ?
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user
|
FROM user
|
||||||
WHERE id like ?
|
WHERE id like ?
|
||||||
",
|
",
|
||||||
@ -315,7 +317,7 @@ WHERE id like ?
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user
|
FROM user
|
||||||
WHERE name like ?
|
WHERE name like ?
|
||||||
",
|
",
|
||||||
@ -357,7 +359,7 @@ WHERE name like ?
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user
|
FROM user
|
||||||
WHERE deleted = 0
|
WHERE deleted = 0
|
||||||
ORDER BY last_access DESC
|
ORDER BY last_access DESC
|
||||||
@ -372,7 +374,7 @@ ORDER BY last_access DESC
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user u
|
FROM user u
|
||||||
JOIN user_role ur ON u.id = ur.user_id
|
JOIN user_role ur ON u.id = ur.user_id
|
||||||
WHERE ur.role_id = ? AND deleted = 0
|
WHERE ur.role_id = ? AND deleted = 0
|
||||||
@ -388,14 +390,14 @@ ORDER BY name;
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf FROM user
|
||||||
WHERE family_id IS NOT NULL
|
WHERE family_id IS NOT NULL
|
||||||
GROUP BY family_id
|
GROUP BY family_id
|
||||||
|
|
||||||
UNION
|
UNION
|
||||||
|
|
||||||
-- Select users with a null family_id, without grouping
|
-- 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 FROM user
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf FROM user
|
||||||
WHERE family_id IS NULL;
|
WHERE family_id IS NULL;
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
@ -408,7 +410,7 @@ WHERE family_id IS NULL;
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user
|
FROM user
|
||||||
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
|
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
@ -423,7 +425,7 @@ ORDER BY name
|
|||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
Self,
|
Self,
|
||||||
"
|
"
|
||||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, membership_pdf
|
||||||
FROM user
|
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
|
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
|
||||||
ORDER BY last_access DESC
|
ORDER BY last_access DESC
|
||||||
@ -441,13 +443,29 @@ ORDER BY last_access DESC
|
|||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) {
|
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) {
|
||||||
let mut family_id = data.family_id;
|
let mut family_id = data.family_id;
|
||||||
|
|
||||||
if family_id.is_some_and(|x| x == -1) {
|
if family_id.is_some_and(|x| x == -1) {
|
||||||
family_id = Some(Family::insert(db).await)
|
family_id = Some(Family::insert(db).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.membership_pdf.is_none() {
|
||||||
|
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)
|
||||||
|
.await
|
||||||
|
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
|
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
|
||||||
data.dob,
|
data.dob,
|
||||||
@ -1046,6 +1064,7 @@ mod test {
|
|||||||
phone: None,
|
phone: None,
|
||||||
address: None,
|
address: None,
|
||||||
family_id: None,
|
family_id: None,
|
||||||
|
membership_pdf: None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -10,8 +10,9 @@ use crate::model::{
|
|||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::Form,
|
form::Form,
|
||||||
|
fs::TempFile,
|
||||||
get,
|
get,
|
||||||
http::Status,
|
http::{ContentType, Status},
|
||||||
post,
|
post,
|
||||||
request::{FlashMessage, FromRequest, Outcome},
|
request::{FlashMessage, FromRequest, Outcome},
|
||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
@ -231,7 +232,7 @@ async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm, Debug)]
|
#[derive(FromForm, Debug)]
|
||||||
pub struct UserEditForm {
|
pub struct UserEditForm<'a> {
|
||||||
pub(crate) id: i32,
|
pub(crate) id: i32,
|
||||||
pub(crate) dob: Option<String>,
|
pub(crate) dob: Option<String>,
|
||||||
pub(crate) weight: Option<String>,
|
pub(crate) weight: Option<String>,
|
||||||
@ -245,12 +246,13 @@ pub struct UserEditForm {
|
|||||||
pub(crate) phone: Option<String>,
|
pub(crate) phone: Option<String>,
|
||||||
pub(crate) address: Option<String>,
|
pub(crate) address: Option<String>,
|
||||||
pub(crate) family_id: Option<i64>,
|
pub(crate) family_id: Option<i64>,
|
||||||
|
pub(crate) membership_pdf: Option<TempFile<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/user", data = "<data>")]
|
#[post("/user", data = "<data>", format = "multipart/form-data")]
|
||||||
async fn update(
|
async fn update(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
data: Form<UserEditForm>,
|
data: Form<UserEditForm<'_>>,
|
||||||
admin: AdminUser,
|
admin: AdminUser,
|
||||||
) -> Flash<Redirect> {
|
) -> Flash<Redirect> {
|
||||||
let user = User::find_by_id(db, data.id).await;
|
let user = User::find_by_id(db, data.id).await;
|
||||||
@ -271,6 +273,25 @@ async fn update(
|
|||||||
Flash::success(Redirect::to("/admin/user"), "Successfully updated user")
|
Flash::success(Redirect::to("/admin/user"), "Successfully updated user")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/user/<user>/membership")]
|
||||||
|
async fn download_membership_pdf(
|
||||||
|
db: &State<SqlitePool>,
|
||||||
|
admin: AdminUser,
|
||||||
|
user: i32,
|
||||||
|
) -> (ContentType, Vec<u8>) {
|
||||||
|
let user = User::find_by_id(db, user).await.unwrap();
|
||||||
|
Log::create(
|
||||||
|
db,
|
||||||
|
format!(
|
||||||
|
"{} downloaded membership application for user: {user:?}",
|
||||||
|
admin.user.name
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
(ContentType::PDF, user.membership_pdf.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(FromForm, Debug)]
|
#[derive(FromForm, Debug)]
|
||||||
struct UserAddForm<'r> {
|
struct UserAddForm<'r> {
|
||||||
name: &'r str,
|
name: &'r str,
|
||||||
@ -307,6 +328,7 @@ pub fn routes() -> Vec<Route> {
|
|||||||
delete,
|
delete,
|
||||||
fees,
|
fees,
|
||||||
fees_paid,
|
fees_paid,
|
||||||
scheckbuch
|
scheckbuch,
|
||||||
|
download_membership_pdf
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
name="name"
|
name="name"
|
||||||
id="filter-js"
|
id="filter-js"
|
||||||
class="search-bar"
|
class="search-bar"
|
||||||
placeholder="Suchen nach (Name, [yes|no]-role:<name>)" />
|
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
|
||||||
</div>
|
</div>
|
||||||
<!-- END filterBar -->
|
<!-- END filterBar -->
|
||||||
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
|
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
|
||||||
@ -41,9 +41,10 @@
|
|||||||
class="text-primary-950 dark:text-white text-right"></div>
|
class="text-primary-950 dark:text-white text-right"></div>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<div data-filterable="true"
|
<div data-filterable="true"
|
||||||
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} ">
|
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %} ">
|
||||||
<form action="/admin/user"
|
<form action="/admin/user"
|
||||||
method="post"
|
method="post"
|
||||||
|
enctype="multipart/form-data"
|
||||||
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
|
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
|
||||||
<div class="w-full grid gap-3">
|
<div class="w-full grid gap-3">
|
||||||
<input type="hidden" name="id" value="{{ user.id }}" />
|
<input type="hidden" name="id" value="{{ user.id }}" />
|
||||||
@ -62,6 +63,12 @@
|
|||||||
{% for role in roles %}
|
{% for role in roles %}
|
||||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
|
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if user.membership_pdf %}
|
||||||
|
<a href="/admin/user/{{ user.id }}/membership"
|
||||||
|
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
|
||||||
|
{% else %}
|
||||||
|
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', id=loop.index, type="file", readonly=allowed_to_edit == false, accept='application/pdf') }}
|
||||||
|
{% endif %}
|
||||||
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
|
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
|
||||||
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
|
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
|
||||||
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
|
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
|
||||||
|
@ -115,7 +115,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="h-8"></div>
|
<div class="h-8"></div>
|
||||||
{% endmacro header %}
|
{% endmacro header %}
|
||||||
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false) %}
|
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='') %}
|
||||||
<div class="{{ wrapper_class }}">
|
<div class="{{ wrapper_class }}">
|
||||||
<label for="{{ name }}"
|
<label for="{{ name }}"
|
||||||
class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">
|
class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">
|
||||||
@ -131,6 +131,7 @@
|
|||||||
placeholder="{% if hide_label %}{{ label }}{% endif %}"
|
placeholder="{% if hide_label %}{{ label }}{% endif %}"
|
||||||
{% if min is defined %}min="{{ min }}"{% endif %}
|
{% if min is defined %}min="{{ min }}"{% endif %}
|
||||||
{% if autofocus %}autofocus{% endif %}
|
{% if autofocus %}autofocus{% endif %}
|
||||||
|
{% if accept %}accept="{{ accept }}"{% endif %}
|
||||||
{% if pattern %}pattern="{{ pattern }}"{% endif %}
|
{% if pattern %}pattern="{{ pattern }}"{% endif %}
|
||||||
{% if readonly %}readonly{% endif %}>
|
{% if readonly %}readonly{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user