use std::collections::HashMap; use askama::Template; use askama_web::WebTemplate; use axum::{ Form, body::Body, extract::{Path, Query, State}, response::{IntoResponse, Redirect, Response}, }; use csv::Writer; use serde::Deserialize; use serde_with::{NoneAsEmptyString, serde_as}; use snafu::prelude::*; use crate::{models::book::Model as BookModel, routes::router::Router, state::error::CSVSnafu}; use crate::{models::user::Model as UserModel, state::error::IOSnafu}; use crate::{ models::{book::BookOperator, user::UserOperator}, state::{ AppState, error::{AppStateError, BookSnafu, UserSnafu}, }, }; // Book list with the owner and the current holder inside struct BookWithUser { pub book: BookModel, pub owner: UserModel, pub current_holder: Option, } /// Query for filter search query #[serde_as] #[derive(Deserialize, Clone, Debug)] pub struct IndexQuery { pub title: Option, pub page: Option, pub authors: Option, #[serde(default)] #[serde_as(as = "NoneAsEmptyString")] pub owner_id: Option, #[serde(default)] #[serde_as(as = "NoneAsEmptyString")] pub current_holder_id: Option, } #[derive(Template, WebTemplate)] #[template(path = "index.html")] struct BookIndexTemplate { books_with_user: Vec, query: IndexQuery, users: Vec, current_page: u64, total_page: u64, base_query: String, router: Router, } pub async fn index( State(state): State, Query(query): Query, ) -> Result { let page: u64 = query .page .map(|p| p.max(1) as u64) // Minimum 1 .unwrap_or(1); // Get all Users let users = UserOperator::new(state.clone()) .all() .await .context(UserSnafu)?; // Get all Book filtered with query let books_paginate = BookOperator::new(state.clone()) .all_paginate(page, Some(query.clone())) .await .context(BookSnafu)?; // Mapping between an user_id and user used in result to // get easily user with his id let user_by_id: HashMap = users .clone() .into_iter() .map(|user| (user.id, user)) .collect(); // Build object of Book with his relation Owner (User) and current_holder (User) let result: Vec = books_paginate .books .into_iter() .filter_map(|book| { let owner = user_by_id.get(&book.owner_id).cloned()?; let current_holder = book .current_holder_id .and_then(|id| user_by_id.get(&id).cloned()); Some(BookWithUser { book, owner, current_holder, }) }) .collect(); // build original search to be sure to keep // search when we change page let mut base_query = String::new(); if let Some(title) = &query.title { base_query.push_str(&format!("title={}&", title)); } if let Some(authors) = &query.authors { base_query.push_str(&format!("authors={}&", authors)); } if let Some(owner_id) = &query.owner_id { base_query.push_str(&format!("owner_id={}&", owner_id)); } if let Some(current_holder_id) = &query.current_holder_id { base_query.push_str(&format!("current_holder_id={}&", current_holder_id)); } Ok(BookIndexTemplate { books_with_user: result, query, users, current_page: books_paginate.current_page, total_page: books_paginate.total_page, base_query, router: Router { base_path: state.config.base_path, }, }) } #[derive(Template, WebTemplate)] #[template(path = "books/show.html")] struct ShowBookTemplate { book: BookModel, owner: UserModel, current_holder: Option, router: Router, } pub async fn show( State(state): State, Path(id): Path, ) -> Result { let book = BookOperator::new(state.clone()) .find_by_id(id) .await .context(BookSnafu)?; let owner = UserOperator::new(state.clone()) .find_by_id(book.owner_id) .await .context(UserSnafu)?; let current_holder: Option = if let Some(current_holder_id) = book.current_holder_id { Some( UserOperator::new(state.clone()) .find_by_id(current_holder_id) .await .context(UserSnafu)?, ) } else { None }; Ok(ShowBookTemplate { book, owner, current_holder, router: Router { base_path: state.config.base_path, }, }) } /// Form to build a new book or an update #[serde_as] #[derive(Deserialize)] pub struct BookForm { pub title: String, pub authors: String, pub owner_id: i32, pub description: Option, pub comment: Option, #[serde_as(as = "NoneAsEmptyString")] pub current_holder_id: Option, } pub async fn create( State(state): State, Form(form): Form, ) -> Result { let _ = BookOperator::new(state) .create(form) .await .context(BookSnafu)?; Ok(Redirect::to("/").into_response()) } #[derive(Template, WebTemplate)] #[template(path = "books/new.html")] struct NewBookTemplate { users: Vec, router: Router, } pub async fn new( State(state): State, ) -> Result { let users = UserOperator::new(state.clone()) .all() .await .context(UserSnafu)?; Ok(NewBookTemplate { users, router: Router { base_path: state.config.base_path, }, }) } #[derive(Template, WebTemplate)] #[template(path = "books/edit.html")] struct EditBookTemplate { users: Vec, book: BookModel, router: Router, } pub async fn edit( State(state): State, Path(id): Path, ) -> Result { let users = UserOperator::new(state.clone()) .all() .await .context(UserSnafu)?; let book = BookOperator::new(state.clone()) .find_by_id(id) .await .context(BookSnafu)?; Ok(EditBookTemplate { users, book, router: Router { base_path: state.config.base_path, }, }) } pub async fn update( State(state): State, Path(id): Path, Form(form): Form, ) -> Result { let _ = BookOperator::new(state) .update(id, form) .await .context(BookSnafu)?; Ok(Redirect::to(&format!("/books/{}", id)).into_response()) } pub async fn delete( State(state): State, Path(id): Path, ) -> Result { let _ = BookOperator::new(state) .delete(id) .await .context(BookSnafu)?; Ok(Redirect::to("/").into_response()) } /// Download CSV filter (no paginate) of all books pub async fn download_csv( State(state): State, Query(query): Query, ) -> Result { let books = BookOperator::new(state.clone()) .all_filtered(Some(query)) .await .context(BookSnafu)?; let users = UserOperator::new(state).all().await.context(UserSnafu)?; let users_by_id: HashMap = users.into_iter().map(|u| (u.id, u)).collect(); let mut wtr = Writer::from_writer(vec![]); wtr.write_record(&[ "ID", "Title", "Author(s)", "Description", "Owner", "Current Holder", "Comment", ]) .context(CSVSnafu)?; for book in books { let owner_format = match users_by_id.get(&book.owner_id).cloned().ok_or(UserSnafu) { Ok(owner) => format!("{} (id: {})", owner.name.to_string(), owner.id), Err(_) => "-".to_string(), }; let current_holder = match users_by_id // if current_holder_id is None, take 0. // So get returns errors because user with id 0 can't exist .get(&book.current_holder_id.unwrap_or(0)) .cloned() .ok_or(UserSnafu) { Ok(current_holder) => format!( "{} (id: {})", current_holder.name.to_string(), current_holder.id ), Err(_) => "-".to_string(), }; wtr.write_record(&[ book.id.to_string(), book.title, book.authors, book.description.unwrap_or_default(), owner_format, current_holder, book.comment.unwrap_or_default(), ]) .context(CSVSnafu)?; } wtr.flush().context(IOSnafu)?; let csv_bytes = wtr.into_inner(); match csv_bytes { Ok(csv_bytes) => Ok(Response::builder() .header("Content-Type", "text/csv") .body(Body::from(csv_bytes)) .unwrap()), Err(_) => Ok(Redirect::to("/").into_response()), } }