Add migration and books

This commit is contained in:
gabatxo1312 2026-01-28 00:38:24 +01:00
parent 30be91390b
commit 4cd32831c1
No known key found for this signature in database
19 changed files with 805 additions and 231 deletions

144
Cargo.lock generated
View File

@ -366,6 +366,7 @@ dependencies = [
"sea-orm", "sea-orm",
"sea-orm-migration", "sea-orm-migration",
"serde", "serde",
"serde_with",
"snafu", "snafu",
"static-serve", "static-serve",
"tokio", "tokio",
@ -612,8 +613,18 @@ version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.11",
"darling_macro", "darling_macro 0.20.11",
]
[[package]]
name = "darling"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core 0.21.3",
"darling_macro 0.21.3",
] ]
[[package]] [[package]]
@ -629,13 +640,38 @@ dependencies = [
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "darling_core"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.114",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.20.11" version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.20.11",
"quote",
"syn 2.0.114",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core 0.21.3",
"quote", "quote",
"syn 2.0.114", "syn 2.0.114",
] ]
@ -739,6 +775,12 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
@ -1260,6 +1302,17 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.13.0"
@ -1268,6 +1321,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.1", "hashbrown 0.16.1",
"serde",
"serde_core",
] ]
[[package]] [[package]]
@ -1876,6 +1931,26 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "ref-cast"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.2" version = "1.12.2"
@ -2006,6 +2081,30 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "schemars"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]]
name = "schemars"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
dependencies = [
"dyn-clone",
"ref-cast",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -2127,7 +2226,7 @@ version = "1.0.0-rc.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "365d236217f5daa4f40d3c9998ff3921351b53472da50308e384388162353b3a" checksum = "365d236217f5daa4f40d3c9998ff3921351b53472da50308e384388162353b3a"
dependencies = [ dependencies = [
"darling", "darling 0.20.11",
"heck 0.4.1", "heck 0.4.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2262,6 +2361,37 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_with"
version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.13.0",
"schemars 0.9.0",
"schemars 1.2.0",
"serde_core",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
dependencies = [
"darling 0.21.3",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -2418,7 +2548,7 @@ dependencies = [
"futures-util", "futures-util",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"hashlink", "hashlink",
"indexmap", "indexmap 2.13.0",
"log", "log",
"memchr", "memchr",
"once_cell", "once_cell",
@ -2839,7 +2969,7 @@ version = "0.9.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
dependencies = [ dependencies = [
"indexmap", "indexmap 2.13.0",
"serde_core", "serde_core",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
@ -2863,7 +2993,7 @@ version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [ dependencies = [
"indexmap", "indexmap 2.13.0",
"toml_datetime", "toml_datetime",
"toml_parser", "toml_parser",
"winnow", "winnow",

View File

@ -30,3 +30,4 @@ dirs = "6.0.0"
pretty_env_logger = "0.5.0" pretty_env_logger = "0.5.0"
# custom logger # custom logger
log = "0.4.29" log = "0.4.29"
serde_with = "3.16.1"

View File

@ -1,6 +1,6 @@
use axum::{ use axum::{
Router, Router,
routing::{delete, get, post}, routing::{get, post},
}; };
use static_serve::embed_assets; use static_serve::embed_assets;
@ -16,7 +16,10 @@ pub fn build_app(state: AppState) -> Router {
Router::new() Router::new()
.route("/", get(routes::book::index)) .route("/", get(routes::book::index))
.route("/books", post(routes::book::create))
.route("/books/{id}", get(routes::book::show)) .route("/books/{id}", get(routes::book::show))
.route("/books/{id}", post(routes::book::update))
.route("/books/{id}/delete", post(routes::book::delete))
.route("/books/{id}/edit", get(routes::book::edit)) .route("/books/{id}/edit", get(routes::book::edit))
.route("/books/new", get(routes::book::new)) .route("/books/new", get(routes::book::new))
.route("/users", get(routes::user::index)) .route("/users", get(routes::user::index))

View File

@ -1,5 +1,7 @@
use sea_orm_migration::{prelude::*, schema::*}; use sea_orm_migration::{prelude::*, schema::*};
use crate::migrations::m20260126_000001_create_user_table::User;
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
pub struct Migration; pub struct Migration;
@ -12,10 +14,24 @@ impl MigrationTrait for Migration {
.table(Book::Table) .table(Book::Table)
.if_not_exists() .if_not_exists()
.col(pk_auto(Book::Id)) .col(pk_auto(Book::Id))
.col(string(Book::Title)) .col(string(Book::Title).not_null())
.col(string(Book::Authors)) .col(string(Book::Authors).not_null())
.col(text(Book::Description)) .col(text(Book::Description))
.col(text(Book::Comment)) .col(text(Book::Comment))
.col(ColumnDef::new(Book::OwnerId).integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fk-book-owner_id")
.from(Book::Table, Book::OwnerId)
.to(User::Table, User::Id),
)
.col(ColumnDef::new(Book::CurrentHolderId).integer())
.foreign_key(
ForeignKey::create()
.name("fk-book-current_holder_id")
.from(Book::Table, Book::CurrentHolderId)
.to(User::Table, User::Id),
)
.to_owned(), .to_owned(),
) )
.await .await
@ -36,4 +52,6 @@ pub enum Book {
Authors, Authors,
Description, Description,
Comment, Comment,
OwnerId,
CurrentHolderId,
} }

122
src/models/book.rs Normal file
View File

@ -0,0 +1,122 @@
use sea_orm::ActiveValue::Set;
use sea_orm::DeleteResult;
use sea_orm::QueryOrder;
use sea_orm::entity::prelude::*;
use snafu::ResultExt;
use snafu::prelude::*;
use crate::routes::book::BookForm;
use crate::state::AppState;
use crate::state::error::BookSnafu;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "book")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub title: String,
pub authors: String,
pub description: Option<String>,
pub comment: Option<String>,
pub owner_id: i32,
#[sea_orm(belongs_to, relation_enum = "Owner", from = "owner_id", to = "id")]
pub owner: HasOne<super::user::Entity>,
pub current_holder_id: Option<i32>,
#[sea_orm(
belongs_to,
relation_enum = "CurrentHolder",
from = "current_holder_id",
to = "id"
)]
pub current_holder: HasOne<super::user::Entity>,
}
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum BookError {
// #[snafu(display("The Content Folder (Path: {path}) does not exist"))]
// NotFound { path: String },
#[snafu(display("Database error"))]
DB { source: sea_orm::DbErr },
#[snafu(display("Book with id {id} not found"))]
NotFound { id: i32 },
}
#[derive(Debug)]
pub struct BookOperator {
pub state: AppState,
}
impl BookOperator {
pub fn new(state: AppState) -> Self {
Self { state }
}
pub async fn list(&self) -> Result<Vec<Model>, BookError> {
Entity::find()
.order_by_desc(Column::Id)
.all(&self.state.db)
.await
.context(DBSnafu)
}
pub async fn find_by_id(&self, id: i32) -> Result<Model, BookError> {
let book_by_id = Entity::find_by_id(id)
.one(&self.state.db)
.await
.context(DBSnafu)?;
if let Some(book) = book_by_id {
Ok(book)
} else {
Err(BookError::NotFound { id })
}
}
pub async fn create(&self, form: BookForm) -> Result<Model, BookError> {
let book = ActiveModel {
title: Set(form.title.clone()),
authors: Set(form.authors.clone()),
owner_id: Set(form.owner_id.clone()),
current_holder_id: Set(form.current_holder_id.clone()),
description: Set(form.description.clone()),
comment: Set(form.comment.clone()),
..Default::default()
};
book.insert(&self.state.db).await.context(DBSnafu)
}
pub async fn update(&self, id: i32, form: BookForm) -> Result<Model, BookError> {
let book_by_id = Self::find_by_id(&self, id).await.context(BookSnafu);
if let Ok(book) = book_by_id {
let mut book: ActiveModel = book.into();
book.title = Set(form.title.clone());
book.authors = Set(form.authors.clone());
book.owner_id = Set(form.owner_id.clone());
book.current_holder_id = Set(form.current_holder_id.clone());
book.description = Set(form.description.clone());
book.comment = Set(form.comment.clone());
book.update(&self.state.db).await.context(DBSnafu)
} else {
Err(BookError::NotFound { id })
}
}
pub async fn delete(&self, id: i32) -> Result<DeleteResult, BookError> {
let book: Option<Model> = Entity::find_by_id(id)
.one(&self.state.db)
.await
.context(DBSnafu)?;
let book: Model = book.unwrap();
book.delete(&self.state.db).await.context(DBSnafu)
}
}

View File

@ -1 +1,2 @@
pub mod book;
pub mod user; pub mod user;

View File

@ -13,8 +13,15 @@ pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i32, pub id: i32,
pub name: String, pub name: String,
// #[sea_orm(has_many)] // #[sea_orm(has_many, relation_enum = "Owner", from = "id", to = "owner_id")]
// pub book: HasMany<super::profile::Entity>, // pub books: HasMany<super::book::Entity>,
// #[sea_orm(
// has_many,
// relation_enum = "CurrentHolder",
// from = "id",
// to = "current_holder_id"
// )]
// pub books_borrowed: HasMany<super::book::Entity>,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@ -27,6 +34,8 @@ pub enum UserError {
// NotFound { path: String }, // NotFound { path: String },
#[snafu(display("Database error"))] #[snafu(display("Database error"))]
DB { source: sea_orm::DbErr }, DB { source: sea_orm::DbErr },
#[snafu(display("User with id {id} not found"))]
NotFound { id: i32 },
} }
#[derive(Debug)] #[derive(Debug)]
@ -43,6 +52,19 @@ impl UserOperator {
Entity::find().all(&self.state.db).await.context(DBSnafu) Entity::find().all(&self.state.db).await.context(DBSnafu)
} }
pub async fn find_by_id(&self, id: i32) -> Result<Model, UserError> {
let user: Option<Model> = Entity::find_by_id(id)
.one(&self.state.db)
.await
.context(DBSnafu)?;
if let Some(user) = user {
Ok(user)
} else {
Err(UserError::NotFound { id })
}
}
pub async fn create(&self, form: UserForm) -> Result<Model, UserError> { pub async fn create(&self, form: UserForm) -> Result<Model, UserError> {
let user = ActiveModel { let user = ActiveModel {
name: Set(form.name), name: Set(form.name),

View File

@ -1,57 +1,196 @@
use std::collections::HashMap;
use askama::Template; use askama::Template;
use askama_web::WebTemplate; use askama_web::WebTemplate;
use axum::extract::Path; use axum::{
Form,
extract::{Path, State},
response::{IntoResponse, Redirect},
};
use serde::Deserialize;
use serde_with::{NoneAsEmptyString, serde_as};
use snafu::prelude::*;
use crate::state::error::AppStateError; use crate::models::book::Model as BookModel;
use crate::models::user::Model as UserModel;
use crate::{
models::{book::BookOperator, user::UserOperator},
state::{
AppState,
error::{AppStateError, BookSnafu, UserSnafu},
},
};
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "index.html")] #[template(path = "index.html")]
struct BookIndexTemplate {} struct BookIndexTemplate {
books_with_user: Vec<BookWithUser>,
}
pub async fn index() -> Result<impl axum::response::IntoResponse, AppStateError> { // Book list with the owner and the current holder inside
if 0 > 1 { struct BookWithUser {
return Err(AppStateError::Error); pub book: BookModel,
pub owner: UserModel,
pub current_holder: Option<UserModel>,
}
pub async fn index(
State(state): State<AppState>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let users = UserOperator::new(state.clone())
.list()
.await
.context(UserSnafu)?;
let books = BookOperator::new(state).list().await.context(BookSnafu)?;
let user_by_id: HashMap<i32, UserModel> =
users.into_iter().map(|user| (user.id, user)).collect();
let mut result: Vec<BookWithUser> = Vec::with_capacity(books.len());
for book in books {
let owner = user_by_id.get(&book.owner_id).cloned().unwrap();
let current_holder = if let Some(current_holder_id) = book.current_holder_id {
user_by_id.get(&current_holder_id).cloned()
} else {
None
};
result.push(BookWithUser {
book,
owner,
current_holder,
});
} }
Ok(BookIndexTemplate {}) Ok(BookIndexTemplate {
books_with_user: result,
})
} }
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "books/show.html")] #[template(path = "books/show.html")]
struct ShowBookTemplate {} struct ShowBookTemplate {
book: BookModel,
owner: UserModel,
current_holder: Option<UserModel>,
}
pub async fn show( pub async fn show(
Path(_id): Path<i32>, State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> { ) -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 { let book = BookOperator::new(state.clone())
return Err(AppStateError::Error); .find_by_id(id)
} .await
.context(BookSnafu)?;
Ok(ShowBookTemplate {}) 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,
})
}
#[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())
} }
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "books/new.html")] #[template(path = "books/new.html")]
struct NewBookTemplate {} struct NewBookTemplate {
users: Vec<UserModel>,
}
pub async fn new() -> Result<impl axum::response::IntoResponse, AppStateError> { pub async fn new(
if 0 > 1 { State(state): State<AppState>,
return Err(AppStateError::Error); ) -> Result<impl axum::response::IntoResponse, AppStateError> {
} let users = UserOperator::new(state).list().await.context(UserSnafu)?;
Ok(NewBookTemplate {}) Ok(NewBookTemplate { users })
} }
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "books/edit.html")] #[template(path = "books/edit.html")]
struct EditBookTemplate {} struct EditBookTemplate {
users: Vec<UserModel>,
book: BookModel,
}
pub async fn edit( pub async fn edit(
Path(_id): Path<i32>, State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> { ) -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 { let users = UserOperator::new(state.clone())
return Err(AppStateError::Error); .list()
} .await
.context(UserSnafu)?;
let book = BookOperator::new(state)
.find_by_id(id)
.await
.context(BookSnafu)?;
Ok(EditBookTemplate {}) 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)?;
Ok(Redirect::to("/").into_response())
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use askama::Template; use askama::Template;
use askama_web::WebTemplate; use askama_web::WebTemplate;
use axum::{ use axum::{
@ -9,25 +11,57 @@ use serde::Deserialize;
use snafu::prelude::*; use snafu::prelude::*;
use crate::{ use crate::{
models::user::{self, UserOperator}, models::{
book::BookOperator,
user::{self, UserOperator},
},
state::{ state::{
AppState, AppState,
error::{AppStateError, UserSnafu}, error::{AppStateError, BookSnafu, UserSnafu},
}, },
}; };
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "users/index.html")] #[template(path = "users/index.html")]
struct UsersIndexTemplate { struct UsersIndexTemplate {
users: Vec<user::Model>, user_with_books_number: Vec<(user::Model, usize, usize)>,
} }
pub async fn index( pub async fn index(
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<impl axum::response::IntoResponse, AppStateError> { ) -> Result<impl axum::response::IntoResponse, AppStateError> {
let users = UserOperator::new(state).list().await.context(UserSnafu)?; let users = UserOperator::new(state.clone())
.list()
.await
.context(UserSnafu)?;
Ok(UsersIndexTemplate { users }) let books = BookOperator::new(state.clone())
.list()
.await
.context(BookSnafu)?;
let mut result: Vec<(user::Model, usize, usize)> = vec![];
let mut owner_books: HashMap<i32, usize> = HashMap::new();
let mut borrowed_books: HashMap<i32, usize> = HashMap::new();
for book in &books {
*owner_books.entry(book.owner_id).or_default() += 1;
if let Some(current_holder_id) = book.current_holder_id {
*borrowed_books.entry(current_holder_id).or_default() += 1;
}
}
for user in users {
let owner_books_size = owner_books.get(&user.id).unwrap_or(&0);
let borrowed_books_size = borrowed_books.get(&user.id).unwrap_or(&0);
result.push((user, *owner_books_size, *borrowed_books_size));
}
Ok(UsersIndexTemplate {
user_with_books_number: result,
})
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@ -3,7 +3,10 @@ use askama_web::WebTemplate;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use snafu::prelude::*; use snafu::prelude::*;
use crate::{models::user::UserError, state::config::ConfigError}; use crate::{
models::{book::BookError, user::UserError},
state::config::ConfigError,
};
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "error.html")] #[template(path = "error.html")]
@ -29,6 +32,10 @@ pub enum AppStateError {
User { User {
source: UserError, source: UserError,
}, },
#[snafu(display("Book Model Error"))]
Book {
source: BookError,
},
} }
impl IntoResponse for AppStateError { impl IntoResponse for AppStateError {

View File

@ -1,49 +1,79 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "components/typography.html" as typography %} {% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
{% block main %} {% block main %}
{{ typography::heading("Editer La petite derniere") }} {{ typography::heading("Editer") }}
<form> {% call cards::card() %}
<div class="mb-3"> <form method="post" action="/books/{{ book.id }}">
<label class="form-label">Name</label> <div class="mb-3">
<input type="text" class="form-control" value="La petite derniere"> <label class="form-label" for="title">Name</label>
</div> <input type="text" name="title" class="form-control" value="{{ book.title }}" required>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Author(s)</label> <label for="authors" class="form-label">Author(s)</label>
<input type="text" class="form-control" value="Fatima Daas"> <input type="text" name="authors" class="form-control" value="{{ book.authors }}" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Owner</label> <label for="owner_id" class="form-label">Owner</label>
<select class="form-control"> <select name="owner_id" class="form-control" required>
<option selected>Jean</option> {% for user in users %}
<option>Simon</option> {% if book.owner_id == user.id %}
</select> <option value="{{ user.id }}" selected>{{ user.name }}</option>
</div> {% else %}
<option value="{{ user.id }}">{{ user.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Current Holder</label> <label for="current_holder_id" class="form-label">Current Holder</label>
<select class="form-control"> <select class="form-control" name="current_holder_id">
<option></option> <option></option>
<option>Jean</option> {% match book.current_holder_id %}
<option selected>Simon</option> {% when Some with (current_holder_id) %}
</select> {% for user in users %}
</div> {% if *current_holder_id == user.id %}
<option value="{{ user.id }}" selected>{{ user.name }}</option>
{% else %}
<option value="{{ user.id }}">{{ user.name }}</option>
{% endif %}
{% endfor %}
{% when None %}
{% for user in users %}
<option value="{{ user.id }}">{{ user.name }}</option>
{% endfor %}
{% endmatch %}
</select>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Description</label> <label for="description" class="form-label">Description</label>
<textarea class="form-control">dsjfskdljf</textarea> {% match book.description %}
</div> {% when Some with (description) %}
<textarea name="description" class="form-control">{{ description }}</textarea>
{% when None %}
<textarea name="description" class="form-control"></textarea>
{% endmatch %}
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Comment</label> <label class="form-label" for="comment">Comment</label>
<textarea class="form-control">sdsfjsdkfjsdklfj</textarea> {% match book.comment %}
</div> {% when Some with (comment) %}
<textarea name="comment" class="form-control">{{ comment }}</textarea>
{% when None %}
<textarea name="comment" class="form-control"></textarea>
{% endmatch %}
</div>
<div class="mt-4 text-center"> <div class="mt-4 text-center">
<input type="submit" value="Create book" class="btn btn-success"> <input type="submit" value="Edit book" class="btn btn-success">
</div> </div>
</form> </form>
{% endblock %} {% endcall %}
{% endblock %}

View File

@ -1,49 +1,54 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "components/typography.html" as typography %} {% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
{% block main %} {% block main %}
{{ typography::heading("New book") }} {{ typography::heading("New book") }}
<form> {% call cards::card() %}
<div class="mb-3"> <form method="post" action="/books">
<label class="form-label">Name</label> <div class="mb-3">
<input type="text" class="form-control"> <label for="title" class="form-label">Name</label>
</div> <input type="text" name="title" class="form-control" required>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Author(s)</label> <label for="authors" class="form-label">Author(s)</label>
<input type="text" class="form-control"> <input type="text" name="authors" class="form-control" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Owner</label> <label for="owner_id" class="form-label">Owner</label>
<select class="form-control"> <select name="owner_id" class="form-control" required>
<option>Jean</option> {% for user in users %}
<option>Simon</option> <option value="{{ user.id }}">{{ user.name }}</option>
</select> {% endfor %}
</div> </select>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Current Holder</label> <label for="current_holder_id" class="form-label">Current Holder</label>
<select class="form-control"> <select name="current_holder_id" class="form-control">
<option></option> <option></option>
<option>Jean</option> {% for user in users %}
<option>Simon</option> <option value="{{ user.id }}">{{ user.name }}</option>
</select> {% endfor %}
</div> </select>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Description</label> <label for="description" class="form-label">Description</label>
<textarea class="form-control"></textarea> <textarea name="description" class="form-control"></textarea>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Comment</label> <label for="comment" class="form-label">Comment</label>
<textarea class="form-control"></textarea> <textarea name="comment" class="form-control"></textarea>
</div> </div>
<div class="mt-4 text-center"> <div class="mt-4 text-center">
<input type="submit" value="Create book" class="btn btn-success"> <input type="submit" value="Create book" class="btn btn-success">
</div> </div>
</form> </form>
{% endblock %} {% endcall %}
{% endblock %}

View File

@ -2,30 +2,42 @@
{% import "components/typography.html" as typography %} {% import "components/typography.html" as typography %}
{% import "components/dropdown.html" as dropdown %} {% import "components/dropdown.html" as dropdown %}
{% import "components/fields.html" as fields %} {% import "components/fields.html" as fields %}
{% import "components/cards.html" as cards %}
{% block main %} {% block main %}
{% call typography::heading("La petite derniere") %} {{ typography::book_heading(book.title, book, show = false) }}
{{ dropdown::dropdown_button("Actions", [("Edit", "/books/1/edit"), ("Delete", "/books/1")]) }}
{% endcall %}
<div class="mt-4"> {% call cards::card() %}
<h5 class="mt-4 fw-bold">Book details</h5> <div class="mt-4">
{{ fields::field("Name", "La petite derniere") }} <h5 class="mt-4 fw-bold">Book details</h5>
{{ fields::field("Authors", "Fatima Daas") }} {{ fields::field("Name", book.title) }}
{{ fields::field("Authors", "Je mappelle Fatima Daas. Je suis la mazoziya, la petite dernière. Celle à laquelle on ne {{ fields::field("Authors", book.authors) }}
sest pas préparé. Française dorigine algérienne. Musulmane pratiquante. Clichoise qui passe plus de trois heures par
jour dans les transports. Une touriste. Une banlieusarde qui observe les comportements parisiens. Je suis une
menteuse, une pécheresse. Adolescente, je suis une élève instable. Adulte, je suis hyper-inadaptée. Jécris des
histoires pour éviter de vivre la mienne. Jai fait quatre ans de thérapie. Cest ma plus longue relation. Lamour,
cétait tabou à la maison, les marques de tendresse, la sexualité aussi. Je me croyais polyamoureuse. Lorsque Nina a
débarqué dans ma vie, je ne savais plus du tout ce dont javais besoin et ce quil me manquait. Je mappelle Fatima
Daas. Je ne sais pas si je porte bien mon prénom.") }}
<h5 class="mt-50px fw-bold">User Details</h5> {% match book.description %}
{{ fields::field("Owner", "Jean") }} {% when Some with (description) %}
{{ fields::field("Current Holder", "Pierre") }} {{ fields::field("Description", description) }}
{% when None %}
{{ fields::field("Description", "-") }}
{% endmatch %}
<h5 class="mt-50px fw-bold">More Informations</h5> <h5 class="mt-50px fw-bold">User Details</h5>
{{ fields::field("Comment", "J'adore ce livre ca parle de plein de choses !") }} {{ fields::field("Owner", owner.name) }}
</div>
{% endblock %} {% match current_holder %}
{% when Some with (current_holder) %}
{{ fields::field("Current Holder", current_holder.name) }}
{% when None %}
{{ fields::field("Current Holder", "-") }}
{% endmatch %}
<h5 class="mt-50px fw-bold">More Informations</h5>
{% match book.comment %}
{% when Some with (comment) %}
{{ fields::field("Comment", comment) }}
{% when None %}
{{ fields::field("Comment", "-") }}
{% endmatch %}
</div>
{% endcall %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% macro card() %}
<div class="card my-3">
<div class="card-body">
{{ caller() }}
</div>
</div>
{% endmacro %}

View File

@ -5,8 +5,27 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% for item in items %} {% for item in items %}
<li><a class="dropdown-item" href="{{ item.1 }}">{{ item.0 }}</a></li> <li><a class="dropdown-item" href="{{ item.1 }}">{{ item.0 }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% endmacro %} {% endmacro %}
{% macro book_dropdown_button(book, show = true) %}
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Actions
</button>
<ul class="dropdown-menu">
{% if show %}
<li><a class="dropdown-item" href="/books/{{ book.id }}">Voir</a></li>
{% endif %}
<li><a class="dropdown-item" href="/books/{{ book.id }}/edit">Edit</a></li>
<li>
<form method="post" action="/books/{{ book.id }}/delete">
<input class="dropdown-item" type="submit" value="Delete">
</form>
</li>
</ul>
</div>
{% endmacro %}

View File

@ -1,14 +1,14 @@
{% macro field(name, value) %} {% macro field(name, value) %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-md-3"> <div class="col-md-3">
<p class="mb-0 fw-regular"> <p class="mb-0 fw-regular">
{{ name }}: {{ name }}:
</p> </p>
</div>
<div class="col-md-9">
<p class="mb-0">
{{ value }}
</p>
</div>
</div> </div>
<div class="col-md-9"> {% endmacro %}
<p class="mb-0">
{{ value }}
</p>
</div>
</div>
{% endmacro %}

View File

@ -1,13 +1,27 @@
{% macro heading(title) %} {% import "components/dropdown.html" as dropdown %}
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-4">
{{ title }}
</h1>
{% if caller is defined %} {% macro heading(title) %}
<div> <div class="d-flex justify-content-between align-items-center">
{{ caller() }} <h1 class="mb-4">
{{ title }}
</h1>
{% if caller is defined %}
<div>
{{ caller() }}
</div>
{% endif %}
</div> </div>
{% endif %} {% endmacro %}
</div>
{% endmacro %} {% macro book_heading(title, book, show = false) %}
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-4">
{{ title }}
</h1>
<div>
{{ dropdown::book_dropdown_button(book, show) }}
</div>
</div>
{% endmacro %}

View File

@ -1,41 +1,44 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "components/typography.html" as typography %} {% import "components/typography.html" as typography %}
{% import "components/dropdown.html" as dropdown %} {% import "components/dropdown.html" as dropdown %}
{% import "components/cards.html" as cards %}
{% block main %} {% block main %}
{{ typography::heading("All books") }} {{ typography::heading("All books") }}
<table class="table table-hover"> {% call cards::card() %}
<thead> <table class="table table-hover">
<tr> <thead>
<th scope="col">#</th> <tr>
<th scope="col">Name</th> <th scope="col">#</th>
<th scope="col">Author(s)</th> <th scope="col">Name</th>
<th scope="col">Owner</th> <th scope="col">Author(s)</th>
<th scope="col">Actions</th> <th scope="col">Owner</th>
</tr> <th scope="col">Current Holder</th>
</thead> <th scope="col">Actions</th>
<tbody> </tr>
<tr> </thead>
<th scope="row">1</th> <tbody>
<td>La petite derniere</td> {% for book_with_user in books_with_user %}
<td>Fatima Daas</td> <tr>
<td>Jean</td> <th scope="row">{{ book_with_user.book.id }}</th>
<td> <td>{{ book_with_user.book.title }}</td>
{{ dropdown::dropdown_button("Actions", [("Voir", "/books/1"), ("Edit", "/books/1/edit"), ("Delete", <td>{{ book_with_user.book.authors }}</td>
"/books/1")]) }} <td>{{ book_with_user.owner.name }}</td>
</td> <td>
</tr> {% match book_with_user.current_holder %}
<tr> {% when Some with (current_holder) %}
<th scope="row">2</th> {{ current_holder.name }}
<td>La petite derniere</td> {% when None %}
<td>Fatima Daas</td> -
<td>Jean</td> {% endmatch %}
<td> </td>
{{ dropdown::dropdown_button("Actions", [("Voir", "/books/1"), ("Edit", "/books/1/edit"), ("Delete", <td>
"/books/1")]) }} {{ dropdown::book_dropdown_button(book_with_user.book) }}
</td> </td>
</tr> </tr>
</tbody> {% endfor %}
</table> </tbody>
</table>
{% endcall %}
{% endblock %} {% endblock %}

View File

@ -1,39 +1,46 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "components/typography.html" as typography %} {% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
{% block main %} {% block main %}
{{ typography::heading("All users") }} {{ typography::heading("All users") }}
<table class="table table-hover"> {% call cards::card() %}
<thead> <table class="table table-hover">
<tr> <thead>
<th scope="col">#</th> <tr>
<th scope="col">Name</th> <th scope="col">#</th>
<th scope="col">Nombre de livres</th> <th scope="col">Name</th>
<th scope="col">Actions</th> <th scope="col">Owner book</th>
</tr> <th scope="col">Borrowed book</th>
</thead> <th scope="col">Actions</th>
<tbody> </tr>
{% for user in users %} </thead>
<tr> <tbody>
<th scope="row">{{ user.id }}</th> {% for (user, book_size, borrowed_book) in user_with_books_number %}
<td>{{ user.name }}</td> <tr>
<td>10</td> <th scope="row">{{ user.id }}</th>
<td> <td>{{ user.name }}</td>
<form method="post" action="/users/{{ user.id }}"> <td>{{ book_size }}</td>
<input value="Delete" type="submit" class="btn btn-danger btn-sm"> <td>{{ borrowed_book }}</td>
</form> <td>
</td> <form method="post" action="/users/{{ user.id }}">
</tr> <input value="Delete" type="submit" class="btn btn-danger btn-sm">
{% endfor %} </form>
</tbody> </td>
</table> </tr>
{% endfor %}
</tbody>
</table>
{% endcall %}
<form action="/users" method="post"> {% call cards::card() %}
<label class="form-label">Name</label> <form action="/users" method="post">
<input type="text" name="name" class="form-control"> <label class="form-label">Name</label>
<input type="text" name="name" class="form-control">
<input type="submit" value="Create user" class="btn btn-success mt-3"> <input type="submit" value="Create user" class="btn btn-success mt-3">
</form> </form>
{% endcall %}
{% endblock %} {% endblock %}