bookforge/src/routes/book.rs

224 lines
5.6 KiB
Rust
Raw Normal View History

2026-01-28 00:38:24 +01:00
use std::collections::HashMap;
2026-01-23 13:56:30 +01:00
use askama::Template;
use askama_web::WebTemplate;
2026-01-28 00:38:24 +01:00
use axum::{
Form,
2026-01-29 00:36:23 +01:00
extract::{Path, Query, State},
2026-01-28 00:38:24 +01:00
response::{IntoResponse, Redirect},
};
use serde::Deserialize;
use serde_with::{NoneAsEmptyString, serde_as};
use snafu::prelude::*;
use crate::models::book::Model as BookModel;
use crate::models::user::Model as UserModel;
2026-01-23 13:56:30 +01:00
2026-01-28 00:38:24 +01:00
use crate::{
models::{book::BookOperator, user::UserOperator},
state::{
AppState,
error::{AppStateError, BookSnafu, UserSnafu},
},
};
2026-01-23 13:56:30 +01:00
2026-01-28 00:38:24 +01:00
// Book list with the owner and the current holder inside
struct BookWithUser {
pub book: BookModel,
pub owner: UserModel,
pub current_holder: Option<UserModel>,
}
2026-01-29 16:52:35 +01:00
/// Query for filter search query
2026-01-29 00:36:23 +01:00
#[serde_as]
#[derive(Deserialize, Clone)]
pub struct BookQuery {
pub title: Option<String>,
pub authors: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub owner_id: Option<i32>,
#[serde_as(as = "NoneAsEmptyString")]
pub current_holder_id: Option<i32>,
}
#[derive(Template, WebTemplate)]
#[template(path = "index.html")]
struct BookIndexTemplate {
books_with_user: Vec<BookWithUser>,
query: BookQuery,
users: Vec<UserModel>,
}
2026-01-28 00:38:24 +01:00
pub async fn index(
State(state): State<AppState>,
2026-01-29 00:36:23 +01:00
Query(query): Query<BookQuery>,
2026-01-28 00:38:24 +01:00
) -> Result<impl axum::response::IntoResponse, AppStateError> {
2026-01-29 16:52:35 +01:00
// Get all Users
2026-01-28 00:38:24 +01:00
let users = UserOperator::new(state.clone())
.list()
.await
.context(UserSnafu)?;
2026-01-29 16:52:35 +01:00
// Get all Book filtered with query
2026-01-29 00:36:23 +01:00
let books = BookOperator::new(state)
.list(Some(query.clone()))
.await
.context(BookSnafu)?;
2026-01-23 13:56:30 +01:00
2026-01-29 16:52:35 +01:00
// Mapping between an user_id and user used in result to
// get easily user with his id
2026-01-29 00:36:23 +01:00
let user_by_id: HashMap<i32, UserModel> = users
.clone()
.into_iter()
.map(|user| (user.id, user))
.collect();
2026-01-28 00:38:24 +01:00
2026-01-29 16:52:35 +01:00
// Build object of Book with his relation Owner (User) and current_holder (User)
2026-01-29 00:36:23 +01:00
let result: Vec<BookWithUser> = 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());
2026-01-28 00:38:24 +01:00
2026-01-29 00:36:23 +01:00
Some(BookWithUser {
book,
owner,
current_holder,
})
})
.collect();
2026-01-23 13:56:30 +01:00
2026-01-28 00:38:24 +01:00
Ok(BookIndexTemplate {
books_with_user: result,
2026-01-29 00:36:23 +01:00
query,
users,
2026-01-28 00:38:24 +01:00
})
2026-01-23 13:56:30 +01:00
}
#[derive(Template, WebTemplate)]
#[template(path = "books/show.html")]
2026-01-28 00:38:24 +01:00
struct ShowBookTemplate {
book: BookModel,
owner: UserModel,
current_holder: Option<UserModel>,
}
2026-01-23 13:56:30 +01:00
pub async fn show(
2026-01-28 00:38:24 +01:00
State(state): State<AppState>,
Path(id): Path<i32>,
2026-01-23 13:56:30 +01:00
) -> Result<impl axum::response::IntoResponse, AppStateError> {
2026-01-28 00:38:24 +01:00
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<UserModel> = 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,
})
}
2026-01-23 13:56:30 +01:00
2026-01-29 16:52:35 +01:00
/// Form to build a new book or an update
2026-01-28 00:38:24 +01:00
#[serde_as]
#[derive(Deserialize)]
pub struct BookForm {
pub title: String,
pub authors: String,
pub owner_id: i32,
pub description: Option<String>,
pub comment: Option<String>,
#[serde_as(as = "NoneAsEmptyString")]
pub current_holder_id: Option<i32>,
}
pub async fn create(
State(state): State<AppState>,
Form(form): Form<BookForm>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let _ = BookOperator::new(state)
.create(form)
.await
.context(BookSnafu)?;
Ok(Redirect::to("/").into_response())
2026-01-23 13:56:30 +01:00
}
#[derive(Template, WebTemplate)]
#[template(path = "books/new.html")]
2026-01-28 00:38:24 +01:00
struct NewBookTemplate {
users: Vec<UserModel>,
}
2026-01-23 13:56:30 +01:00
2026-01-28 00:38:24 +01:00
pub async fn new(
State(state): State<AppState>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let users = UserOperator::new(state).list().await.context(UserSnafu)?;
2026-01-23 13:56:30 +01:00
2026-01-28 00:38:24 +01:00
Ok(NewBookTemplate { users })
2026-01-23 13:56:30 +01:00
}
#[derive(Template, WebTemplate)]
#[template(path = "books/edit.html")]
2026-01-28 00:38:24 +01:00
struct EditBookTemplate {
users: Vec<UserModel>,
book: BookModel,
}
2026-01-23 13:56:30 +01:00
pub async fn edit(
2026-01-28 00:38:24 +01:00
State(state): State<AppState>,
Path(id): Path<i32>,
2026-01-23 13:56:30 +01:00
) -> Result<impl axum::response::IntoResponse, AppStateError> {
2026-01-28 00:38:24 +01:00
let users = UserOperator::new(state.clone())
.list()
.await
.context(UserSnafu)?;
let book = BookOperator::new(state)
.find_by_id(id)
.await
.context(BookSnafu)?;
Ok(EditBookTemplate { users, book })
}
pub async fn update(
State(state): State<AppState>,
Path(id): Path<i32>,
Form(form): Form<BookForm>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
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<AppState>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let _ = BookOperator::new(state)
.delete(id)
.await
.context(BookSnafu)?;
2026-01-23 13:56:30 +01:00
2026-01-28 00:38:24 +01:00
Ok(Redirect::to("/").into_response())
2026-01-23 13:56:30 +01:00
}