Add migration and books
This commit is contained in:
parent
30be91390b
commit
4cd32831c1
19 changed files with 819 additions and 245 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(¤t_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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue