notification #292
| @@ -16,7 +16,8 @@ CREATE TABLE IF NOT EXISTS "user" ( | ||||
| 	"notes" text, | ||||
| 	"phone" text, | ||||
| 	"address" text, | ||||
| 	"family_id" INTEGER REFERENCES family(id) | ||||
| 	"family_id" INTEGER REFERENCES family(id), | ||||
| 	"membership_pdf" BLOB | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "family" ( | ||||
|   | ||||
| @@ -75,7 +75,7 @@ GROUP BY family.id;" | ||||
|     } | ||||
|  | ||||
|     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) | ||||
|             .await | ||||
|             .unwrap() | ||||
|   | ||||
| @@ -16,7 +16,7 @@ impl Rower { | ||||
|         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 | ||||
| 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 id in (SELECT rower_id FROM rower WHERE logbook_id=?) | ||||
|         ", | ||||
|   | ||||
| @@ -8,6 +8,7 @@ use rocket::{ | ||||
|     http::{Cookie, Status}, | ||||
|     request::{self, FromRequest, Outcome}, | ||||
|     time::{Duration, OffsetDateTime}, | ||||
|     tokio::io::AsyncReadExt, | ||||
|     Request, | ||||
| }; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| @@ -43,6 +44,7 @@ pub struct User { | ||||
|     pub phone: Option<String>, | ||||
|     pub address: Option<String>, | ||||
|     pub family_id: Option<i64>, | ||||
|     pub membership_pdf: Option<Vec<u8>>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| @@ -285,7 +287,7 @@ impl User { | ||||
|         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 | ||||
| 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 id like ? | ||||
|         ", | ||||
| @@ -300,7 +302,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 | ||||
| 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 id like ? | ||||
|         ", | ||||
| @@ -315,7 +317,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 | ||||
| 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 name like ? | ||||
|         ", | ||||
| @@ -357,7 +359,7 @@ WHERE name 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 | ||||
| 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 deleted = 0 | ||||
| ORDER BY last_access DESC | ||||
| @@ -372,7 +374,7 @@ 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 | ||||
| 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 | ||||
| JOIN user_role ur ON u.id = ur.user_id | ||||
| WHERE ur.role_id = ? AND deleted = 0 | ||||
| @@ -388,14 +390,14 @@ ORDER BY 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 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 | ||||
| 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  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; | ||||
|         " | ||||
|         ) | ||||
| @@ -408,7 +410,7 @@ WHERE family_id IS NULL; | ||||
|         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 | ||||
| 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 deleted = 0 AND dob != '' and weight != '' and sex != '' | ||||
| ORDER BY name  | ||||
| @@ -423,7 +425,7 @@ ORDER BY 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 | ||||
| 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 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 | ||||
| @@ -441,13 +443,29 @@ ORDER BY last_access DESC | ||||
|             .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; | ||||
|  | ||||
|         if family_id.is_some_and(|x| x == -1) { | ||||
|             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!( | ||||
|             "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?", | ||||
|             data.dob, | ||||
| @@ -1046,6 +1064,7 @@ mod test { | ||||
|                 phone: None, | ||||
|                 address: None, | ||||
|                 family_id: None, | ||||
|                 membership_pdf: None, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|   | ||||
| @@ -10,8 +10,9 @@ use crate::model::{ | ||||
| use futures::future::join_all; | ||||
| use rocket::{ | ||||
|     form::Form, | ||||
|     fs::TempFile, | ||||
|     get, | ||||
|     http::Status, | ||||
|     http::{ContentType, Status}, | ||||
|     post, | ||||
|     request::{FlashMessage, FromRequest, Outcome}, | ||||
|     response::{Flash, Redirect}, | ||||
| @@ -231,7 +232,7 @@ async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Re | ||||
| } | ||||
|  | ||||
| #[derive(FromForm, Debug)] | ||||
| pub struct UserEditForm { | ||||
| pub struct UserEditForm<'a> { | ||||
|     pub(crate) id: i32, | ||||
|     pub(crate) dob: Option<String>, | ||||
|     pub(crate) weight: Option<String>, | ||||
| @@ -245,12 +246,13 @@ pub struct UserEditForm { | ||||
|     pub(crate) phone: Option<String>, | ||||
|     pub(crate) address: Option<String>, | ||||
|     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( | ||||
|     db: &State<SqlitePool>, | ||||
|     data: Form<UserEditForm>, | ||||
|     data: Form<UserEditForm<'_>>, | ||||
|     admin: AdminUser, | ||||
| ) -> Flash<Redirect> { | ||||
|     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") | ||||
| } | ||||
|  | ||||
| #[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)] | ||||
| struct UserAddForm<'r> { | ||||
|     name: &'r str, | ||||
| @@ -307,6 +328,7 @@ pub fn routes() -> Vec<Route> { | ||||
|         delete, | ||||
|         fees, | ||||
|         fees_paid, | ||||
|         scheckbuch | ||||
|         scheckbuch, | ||||
|         download_membership_pdf | ||||
|     ] | ||||
| } | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|                    name="name" | ||||
|                    id="filter-js" | ||||
|                    class="search-bar" | ||||
|                    placeholder="Suchen nach (Name, [yes|no]-role:<name>)" /> | ||||
|                    placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" /> | ||||
|         </div> | ||||
|         <!-- END filterBar --> | ||||
|         <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> | ||||
|             {% for user in users %} | ||||
|                 <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" | ||||
|                           method="post" | ||||
|                           enctype="multipart/form-data" | ||||
|                           class="bg-white dark:bg-primary-900 p-3 rounded-md w-full"> | ||||
|                         <div class="w-full grid gap-3"> | ||||
|                             <input type="hidden" name="id" value="{{ user.id }}" /> | ||||
| @@ -62,6 +63,12 @@ | ||||
|                                 {% 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) }} | ||||
|                                 {% 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='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) }} | ||||
|   | ||||
| @@ -115,7 +115,7 @@ | ||||
|         </header> | ||||
|         <div class="h-8"></div> | ||||
|     {% 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 }}"> | ||||
|             <label for="{{ name }}" | ||||
|                    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 %}" | ||||
|                    {% if min is defined %}min="{{ min }}"{% endif %} | ||||
|                    {% if autofocus %}autofocus{% endif %} | ||||
|                    {% if accept %}accept="{{ accept }}"{% endif %} | ||||
|                    {% if pattern %}pattern="{{ pattern }}"{% endif %} | ||||
|                    {% if readonly %}readonly{% endif %}> | ||||
|         </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user