notification #292
| @@ -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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user