Add migration and books

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

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 {}
struct BookIndexTemplate {
books_with_user: Vec<BookWithUser>,
}
pub async fn index() -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 {
return Err(AppStateError::Error);
// 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 {})
Ok(BookIndexTemplate {
books_with_user: result,
})
}
#[derive(Template, WebTemplate)]
#[template(path = "books/show.html")]
struct ShowBookTemplate {}
struct ShowBookTemplate {
book: BookModel,
owner: UserModel,
current_holder: Option<UserModel>,
}
pub async fn show(
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 book = BookOperator::new(state.clone())
.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)]
#[template(path = "books/new.html")]
struct NewBookTemplate {}
struct NewBookTemplate {
users: Vec<UserModel>,
}
pub async fn new() -> Result<impl axum::response::IntoResponse, AppStateError> {
if 0 > 1 {
return Err(AppStateError::Error);
}
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 {})
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 {})
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_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 {