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-migration",
"serde",
"serde_with",
"snafu",
"static-serve",
"tokio",
@ -612,8 +613,18 @@ version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.20.11",
"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]]
@ -629,13 +640,38 @@ dependencies = [
"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]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
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",
"syn 2.0.114",
]
@ -739,6 +775,12 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
@ -1260,6 +1302,17 @@ dependencies = [
"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]]
name = "indexmap"
version = "2.13.0"
@ -1268,6 +1321,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]]
@ -1876,6 +1931,26 @@ dependencies = [
"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]]
name = "regex"
version = "1.12.2"
@ -2006,6 +2081,30 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "scopeguard"
version = "1.2.0"
@ -2127,7 +2226,7 @@ version = "1.0.0-rc.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "365d236217f5daa4f40d3c9998ff3921351b53472da50308e384388162353b3a"
dependencies = [
"darling",
"darling 0.20.11",
"heck 0.4.1",
"proc-macro2",
"quote",
@ -2262,6 +2361,37 @@ dependencies = [
"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]]
name = "sha1"
version = "0.10.6"
@ -2418,7 +2548,7 @@ dependencies = [
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"indexmap 2.13.0",
"log",
"memchr",
"once_cell",
@ -2839,7 +2969,7 @@ version = "0.9.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
dependencies = [
"indexmap",
"indexmap 2.13.0",
"serde_core",
"serde_spanned",
"toml_datetime",
@ -2863,7 +2993,7 @@ version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap",
"indexmap 2.13.0",
"toml_datetime",
"toml_parser",
"winnow",

View File

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

View File

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

View File

@ -1,5 +1,7 @@
use sea_orm_migration::{prelude::*, schema::*};
use crate::migrations::m20260126_000001_create_user_table::User;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -12,10 +14,24 @@ impl MigrationTrait for Migration {
.table(Book::Table)
.if_not_exists()
.col(pk_auto(Book::Id))
.col(string(Book::Title))
.col(string(Book::Authors))
.col(string(Book::Title).not_null())
.col(string(Book::Authors).not_null())
.col(text(Book::Description))
.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(),
)
.await
@ -36,4 +52,6 @@ pub enum Book {
Authors,
Description,
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;

View File

@ -13,8 +13,15 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
// #[sea_orm(has_many)]
// pub book: HasMany<super::profile::Entity>,
// #[sea_orm(has_many, relation_enum = "Owner", from = "id", to = "owner_id")]
// 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]
@ -27,6 +34,8 @@ pub enum UserError {
// NotFound { path: String },
#[snafu(display("Database error"))]
DB { source: sea_orm::DbErr },
#[snafu(display("User with id {id} not found"))]
NotFound { id: i32 },
}
#[derive(Debug)]
@ -43,6 +52,19 @@ impl UserOperator {
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> {
let user = ActiveModel {
name: Set(form.name),

View File

@ -1,57 +1,196 @@
use std::collections::HashMap;
use askama::Template;
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)]
#[template(path = "index.html")]
struct BookIndexTemplate {}
pub async fn index() -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 {
return Err(AppStateError::Error);
struct BookIndexTemplate {
books_with_user: Vec<BookWithUser>,
}
Ok(BookIndexTemplate {})
// Book list with the owner and the current holder inside
struct BookWithUser {
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 {
books_with_user: result,
})
}
#[derive(Template, WebTemplate)]
#[template(path = "books/show.html")]
struct ShowBookTemplate {}
pub async fn show(
Path(_id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 {
return Err(AppStateError::Error);
struct ShowBookTemplate {
book: BookModel,
owner: UserModel,
current_holder: Option<UserModel>,
}
Ok(ShowBookTemplate {})
pub async fn show(
State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
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,
})
}
#[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)]
#[template(path = "books/new.html")]
struct NewBookTemplate {}
pub async fn new() -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 {
return Err(AppStateError::Error);
struct NewBookTemplate {
users: Vec<UserModel>,
}
Ok(NewBookTemplate {})
pub async fn new(
State(state): State<AppState>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let users = UserOperator::new(state).list().await.context(UserSnafu)?;
Ok(NewBookTemplate { users })
}
#[derive(Template, WebTemplate)]
#[template(path = "books/edit.html")]
struct EditBookTemplate {}
struct EditBookTemplate {
users: Vec<UserModel>,
book: BookModel,
}
pub async fn edit(
Path(_id): Path<i32>,
State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 {
return Err(AppStateError::Error);
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 })
}
Ok(EditBookTemplate {})
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_web::WebTemplate;
use axum::{
@ -9,25 +11,57 @@ use serde::Deserialize;
use snafu::prelude::*;
use crate::{
models::user::{self, UserOperator},
models::{
book::BookOperator,
user::{self, UserOperator},
},
state::{
AppState,
error::{AppStateError, UserSnafu},
error::{AppStateError, BookSnafu, UserSnafu},
},
};
#[derive(Template, WebTemplate)]
#[template(path = "users/index.html")]
struct UsersIndexTemplate {
users: Vec<user::Model>,
user_with_books_number: Vec<(user::Model, usize, usize)>,
}
pub async fn index(
State(state): State<AppState>,
) -> 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)]

View File

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

View File

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

View File

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

View File

@ -2,30 +2,42 @@
{% import "components/typography.html" as typography %}
{% import "components/dropdown.html" as dropdown %}
{% import "components/fields.html" as fields %}
{% import "components/cards.html" as cards %}
{% block main %}
{% call typography::heading("La petite derniere") %}
{{ dropdown::dropdown_button("Actions", [("Edit", "/books/1/edit"), ("Delete", "/books/1")]) }}
{% endcall %}
{{ typography::book_heading(book.title, book, show = false) }}
{% call cards::card() %}
<div class="mt-4">
<h5 class="mt-4 fw-bold">Book details</h5>
{{ fields::field("Name", "La petite derniere") }}
{{ fields::field("Authors", "Fatima Daas") }}
{{ fields::field("Authors", "Je mappelle Fatima Daas. Je suis la mazoziya, la petite dernière. Celle à laquelle on ne
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.") }}
{{ fields::field("Name", book.title) }}
{{ fields::field("Authors", book.authors) }}
{% match book.description %}
{% when Some with (description) %}
{{ fields::field("Description", description) }}
{% when None %}
{{ fields::field("Description", "-") }}
{% endmatch %}
<h5 class="mt-50px fw-bold">User Details</h5>
{{ fields::field("Owner", "Jean") }}
{{ fields::field("Current Holder", "Pierre") }}
{{ fields::field("Owner", owner.name) }}
{% 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>
{{ fields::field("Comment", "J'adore ce livre ca parle de plein de choses !") }}
{% 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

@ -10,3 +10,22 @@
</ul>
</div>
{% 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,3 +1,5 @@
{% import "components/dropdown.html" as dropdown %}
{% macro heading(title) %}
<div class="d-flex justify-content-between align-items-center">
<h1 class="mb-4">
@ -11,3 +13,15 @@
{% endif %}
</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,10 +1,12 @@
{% extends "base.html" %}
{% import "components/typography.html" as typography %}
{% import "components/dropdown.html" as dropdown %}
{% import "components/cards.html" as cards %}
{% block main %}
{{ typography::heading("All books") }}
{% call cards::card() %}
<table class="table table-hover">
<thead>
<tr>
@ -12,30 +14,31 @@
<th scope="col">Name</th>
<th scope="col">Author(s)</th>
<th scope="col">Owner</th>
<th scope="col">Current Holder</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for book_with_user in books_with_user %}
<tr>
<th scope="row">1</th>
<td>La petite derniere</td>
<td>Fatima Daas</td>
<td>Jean</td>
<th scope="row">{{ book_with_user.book.id }}</th>
<td>{{ book_with_user.book.title }}</td>
<td>{{ book_with_user.book.authors }}</td>
<td>{{ book_with_user.owner.name }}</td>
<td>
{{ dropdown::dropdown_button("Actions", [("Voir", "/books/1"), ("Edit", "/books/1/edit"), ("Delete",
"/books/1")]) }}
</td>
</tr>
<tr>
<th scope="row">2</th>
<td>La petite derniere</td>
<td>Fatima Daas</td>
<td>Jean</td>
<td>
{{ dropdown::dropdown_button("Actions", [("Voir", "/books/1"), ("Edit", "/books/1/edit"), ("Delete",
"/books/1")]) }}
{% match book_with_user.current_holder %}
{% when Some with (current_holder) %}
{{ current_holder.name }}
{% when None %}
-
{% endmatch %}
</td>
<td>
{{ dropdown::book_dropdown_button(book_with_user.book) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endcall %}
{% endblock %}

View File

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