Add migration and books
This commit is contained in:
parent
30be91390b
commit
4cd32831c1
144
Cargo.lock
generated
144
Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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
122
src/models/book.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
|
pub mod book;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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(¤t_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())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)]
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 m’appelle Fatima Daas. Je suis la mazoziya, la petite dernière. Celle à laquelle on ne
|
{{ fields::field("Authors", book.authors) }}
|
||||||
s’est pas préparé. Française d’origine 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. J’ai fait quatre ans de thérapie. C’est ma plus longue relation. L’amour,
|
|
||||||
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 j’avais besoin et ce qu’il me manquait. Je m’appelle 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 %}
|
||||||
|
|||||||
7
templates/components/cards.html
Normal file
7
templates/components/cards.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{% macro card() %}
|
||||||
|
<div class="card my-3">
|
||||||
|
<div class="card-body">
|
||||||
|
{{ caller() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user