Compare commits
3 Commits
85a9e9d440
...
3454fb503d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3454fb503d | ||
|
|
374d318dea | ||
|
|
673fd2c58a |
@ -1,4 +1,5 @@
|
|||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::Condition;
|
||||||
use sea_orm::DeleteResult;
|
use sea_orm::DeleteResult;
|
||||||
use sea_orm::QueryOrder;
|
use sea_orm::QueryOrder;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
@ -6,6 +7,7 @@ use snafu::ResultExt;
|
|||||||
use snafu::prelude::*;
|
use snafu::prelude::*;
|
||||||
|
|
||||||
use crate::routes::book::BookForm;
|
use crate::routes::book::BookForm;
|
||||||
|
use crate::routes::book::IndexQuery;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::state::error::BookSnafu;
|
use crate::state::error::BookSnafu;
|
||||||
|
|
||||||
@ -38,15 +40,16 @@ impl ActiveModelBehavior for ActiveModel {}
|
|||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[snafu(visibility(pub))]
|
#[snafu(visibility(pub))]
|
||||||
pub enum BookError {
|
pub enum BookError {
|
||||||
// #[snafu(display("The Content Folder (Path: {path}) does not exist"))]
|
/// Db Error from SeaOrm
|
||||||
// NotFound { path: String },
|
|
||||||
#[snafu(display("Database error"))]
|
#[snafu(display("Database error"))]
|
||||||
DB { source: sea_orm::DbErr },
|
DB { source: sea_orm::DbErr },
|
||||||
|
/// When Book with Id is not found
|
||||||
#[snafu(display("Book with id {id} not found"))]
|
#[snafu(display("Book with id {id} not found"))]
|
||||||
NotFound { id: i32 },
|
NotFound { id: i32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
/// Operator for the CRUD on Book Model
|
||||||
pub struct BookOperator {
|
pub struct BookOperator {
|
||||||
pub state: AppState,
|
pub state: AppState,
|
||||||
}
|
}
|
||||||
@ -59,10 +62,14 @@ pub struct BooksPaginate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BookOperator {
|
impl BookOperator {
|
||||||
|
/// Creates a new `BookOperator` with the given application state.
|
||||||
pub fn new(state: AppState) -> Self {
|
pub fn new(state: AppState) -> Self {
|
||||||
Self { state }
|
Self { state }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lists all books matching the optional query filters.
|
||||||
|
///
|
||||||
|
/// Results are ordered by ID in descending order (newest first).
|
||||||
pub async fn list(&self) -> Result<Vec<Model>, BookError> {
|
pub async fn list(&self) -> Result<Vec<Model>, BookError> {
|
||||||
Entity::find()
|
Entity::find()
|
||||||
.order_by_desc(Column::Id)
|
.order_by_desc(Column::Id)
|
||||||
@ -71,13 +78,37 @@ impl BookOperator {
|
|||||||
.context(DBSnafu)
|
.context(DBSnafu)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_paginate(&self, page: u64) -> Result<BooksPaginate, BookError> {
|
pub async fn list_paginate(
|
||||||
|
&self,
|
||||||
|
page: u64,
|
||||||
|
query: Option<IndexQuery>,
|
||||||
|
) -> Result<BooksPaginate, BookError> {
|
||||||
let page = if page > 0 { page } else { 1 }; // keep 1-indexed
|
let page = if page > 0 { page } else { 1 }; // keep 1-indexed
|
||||||
let page_0indexed = page - 1; // convert for SeaORM (0-based index)
|
let page_0indexed = page - 1; // convert for SeaORM (0-based index)
|
||||||
|
|
||||||
|
let mut conditions = Condition::all();
|
||||||
|
if let Some(book_query) = query {
|
||||||
|
if let Some(title) = book_query.title {
|
||||||
|
conditions = conditions.add(Column::Title.contains(&title));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(authors) = book_query.authors {
|
||||||
|
conditions = conditions.add(Column::Authors.contains(&authors));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(owner_id) = book_query.owner_id {
|
||||||
|
conditions = conditions.add(Column::OwnerId.eq(owner_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(current_holder_id) = book_query.current_holder_id {
|
||||||
|
conditions = conditions.add(Column::CurrentHolderId.eq(current_holder_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let book_pages = Entity::find()
|
let book_pages = Entity::find()
|
||||||
|
.filter(conditions)
|
||||||
.order_by_desc(Column::Id)
|
.order_by_desc(Column::Id)
|
||||||
.paginate(&self.state.db, 100);
|
.paginate(&self.state.db, 1);
|
||||||
|
|
||||||
let books = book_pages
|
let books = book_pages
|
||||||
.fetch_page(page_0indexed)
|
.fetch_page(page_0indexed)
|
||||||
@ -92,6 +123,10 @@ impl BookOperator {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finds a book by its ID.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `BookError::NotFound` if no book exists with the given ID.
|
||||||
pub async fn find_by_id(&self, id: i32) -> Result<Model, BookError> {
|
pub async fn find_by_id(&self, id: i32) -> Result<Model, BookError> {
|
||||||
let book_by_id = Entity::find_by_id(id)
|
let book_by_id = Entity::find_by_id(id)
|
||||||
.one(&self.state.db)
|
.one(&self.state.db)
|
||||||
@ -105,6 +140,7 @@ impl BookOperator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new book from the given form data.
|
||||||
pub async fn create(&self, form: BookForm) -> Result<Model, BookError> {
|
pub async fn create(&self, form: BookForm) -> Result<Model, BookError> {
|
||||||
let book = ActiveModel {
|
let book = ActiveModel {
|
||||||
title: Set(form.title.clone()),
|
title: Set(form.title.clone()),
|
||||||
@ -119,6 +155,10 @@ impl BookOperator {
|
|||||||
book.insert(&self.state.db).await.context(DBSnafu)
|
book.insert(&self.state.db).await.context(DBSnafu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a book (find with ID) from the given form data
|
||||||
|
///
|
||||||
|
/// # Error
|
||||||
|
/// Returns BookError::NotFound if id is not found in database
|
||||||
pub async fn update(&self, id: i32, form: BookForm) -> Result<Model, BookError> {
|
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);
|
let book_by_id = Self::find_by_id(&self, id).await.context(BookSnafu);
|
||||||
|
|
||||||
@ -138,6 +178,7 @@ impl BookOperator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a book (find with ID)
|
||||||
pub async fn delete(&self, id: i32) -> Result<DeleteResult, BookError> {
|
pub async fn delete(&self, id: i32) -> Result<DeleteResult, BookError> {
|
||||||
let book: Option<Model> = Entity::find_by_id(id)
|
let book: Option<Model> = Entity::find_by_id(id)
|
||||||
.one(&self.state.db)
|
.one(&self.state.db)
|
||||||
|
|||||||
@ -22,14 +22,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Template, WebTemplate)]
|
|
||||||
#[template(path = "index.html")]
|
|
||||||
struct BookIndexTemplate {
|
|
||||||
books_with_user: Vec<BookWithUser>,
|
|
||||||
current_page: u64,
|
|
||||||
total_page: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Book list with the owner and the current holder inside
|
// Book list with the owner and the current holder inside
|
||||||
struct BookWithUser {
|
struct BookWithUser {
|
||||||
pub book: BookModel,
|
pub book: BookModel,
|
||||||
@ -37,53 +29,102 @@ struct BookWithUser {
|
|||||||
pub current_holder: Option<UserModel>,
|
pub current_holder: Option<UserModel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
/// Query for filter search query
|
||||||
pub struct Pagination {
|
#[serde_as]
|
||||||
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
pub struct IndexQuery {
|
||||||
|
pub title: Option<String>,
|
||||||
pub page: Option<usize>,
|
pub page: Option<usize>,
|
||||||
|
pub authors: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "NoneAsEmptyString")]
|
||||||
|
pub owner_id: Option<i32>,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "NoneAsEmptyString")]
|
||||||
|
pub current_holder_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template, WebTemplate)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
struct BookIndexTemplate {
|
||||||
|
books_with_user: Vec<BookWithUser>,
|
||||||
|
query: IndexQuery,
|
||||||
|
users: Vec<UserModel>,
|
||||||
|
current_page: u64,
|
||||||
|
total_page: u64,
|
||||||
|
base_query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index(
|
pub async fn index(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(pagination): Query<Pagination>,
|
Query(query): Query<IndexQuery>,
|
||||||
) -> Result<impl axum::response::IntoResponse, AppStateError> {
|
) -> Result<impl axum::response::IntoResponse, AppStateError> {
|
||||||
let page: u64 = pagination
|
let page: u64 = query
|
||||||
.page
|
.page
|
||||||
.map(|p| p.max(1) as u64) // Minimum 1
|
.map(|p| p.max(1) as u64) // Minimum 1
|
||||||
.unwrap_or(1);
|
.unwrap_or(1);
|
||||||
|
|
||||||
|
// Get all Users
|
||||||
let users = UserOperator::new(state.clone())
|
let users = UserOperator::new(state.clone())
|
||||||
.list()
|
.list()
|
||||||
.await
|
.await
|
||||||
.context(UserSnafu)?;
|
.context(UserSnafu)?;
|
||||||
|
|
||||||
|
// Get all Book filtered with query
|
||||||
let books_paginate = BookOperator::new(state)
|
let books_paginate = BookOperator::new(state)
|
||||||
.list_paginate(page)
|
.list_paginate(page, Some(query.clone()))
|
||||||
.await
|
.await
|
||||||
.context(BookSnafu)?;
|
.context(BookSnafu)?;
|
||||||
|
|
||||||
let user_by_id: HashMap<i32, UserModel> =
|
// Mapping between an user_id and user used in result to
|
||||||
users.into_iter().map(|user| (user.id, user)).collect();
|
// get easily user with his id
|
||||||
|
let user_by_id: HashMap<i32, UserModel> = users
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.map(|user| (user.id, user))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut result: Vec<BookWithUser> = Vec::with_capacity(books_paginate.books.len());
|
// Build object of Book with his relation Owner (User) and current_holder (User)
|
||||||
|
let result: Vec<BookWithUser> = books_paginate
|
||||||
|
.books
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|book| {
|
||||||
|
let owner = user_by_id.get(&book.owner_id).cloned()?;
|
||||||
|
let current_holder = book
|
||||||
|
.current_holder_id
|
||||||
|
.and_then(|id| user_by_id.get(&id).cloned());
|
||||||
|
|
||||||
for book in books_paginate.books {
|
Some(BookWithUser {
|
||||||
let owner = user_by_id.get(&book.owner_id).cloned().unwrap();
|
book,
|
||||||
let current_holder = if let Some(current_holder_id) = book.current_holder_id {
|
owner,
|
||||||
user_by_id.get(¤t_holder_id).cloned()
|
current_holder,
|
||||||
} else {
|
})
|
||||||
None
|
})
|
||||||
};
|
.collect();
|
||||||
|
|
||||||
result.push(BookWithUser {
|
// build original search to be sure to keep
|
||||||
book,
|
// search when we change page
|
||||||
owner,
|
let mut base_query = String::new();
|
||||||
current_holder,
|
if let Some(title) = &query.title {
|
||||||
});
|
base_query.push_str(&format!("title={}&", title));
|
||||||
|
}
|
||||||
|
if let Some(authors) = &query.authors {
|
||||||
|
base_query.push_str(&format!("authors={}&", authors));
|
||||||
|
}
|
||||||
|
if let Some(owner_id) = &query.owner_id {
|
||||||
|
base_query.push_str(&format!("owner_id={}&", owner_id));
|
||||||
|
}
|
||||||
|
if let Some(current_holder_id) = &query.current_holder_id {
|
||||||
|
base_query.push_str(&format!("current_holder_id={}&", current_holder_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BookIndexTemplate {
|
Ok(BookIndexTemplate {
|
||||||
books_with_user: result,
|
books_with_user: result,
|
||||||
|
query,
|
||||||
|
users,
|
||||||
current_page: books_paginate.current_page,
|
current_page: books_paginate.current_page,
|
||||||
total_page: books_paginate.total_page,
|
total_page: books_paginate.total_page,
|
||||||
|
base_query,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +169,7 @@ pub async fn show(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Form to build a new book or an update
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct BookForm {
|
pub struct BookForm {
|
||||||
|
|||||||
@ -6,6 +6,81 @@
|
|||||||
{% block main %}
|
{% block main %}
|
||||||
{{ typography::heading("All books") }}
|
{{ typography::heading("All books") }}
|
||||||
|
|
||||||
|
{% call cards::card() %}
|
||||||
|
<form method="get">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="title" class="form-label">Name</label>
|
||||||
|
{% match query.title %}
|
||||||
|
{% when Some with (value) %}
|
||||||
|
<input type="text" name="title" value="{{ value }}" class="form-control" placeholder="Ex: La liberte ou rien">
|
||||||
|
{% when None %}
|
||||||
|
<input type="text" name="title" class="form-control" placeholder="Ex: La liberte ou rien">
|
||||||
|
{% endmatch %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="authors" class="form-label">Authors</label>
|
||||||
|
|
||||||
|
{% match query.authors %}
|
||||||
|
{% when Some with (value) %}
|
||||||
|
<input type="text" name="authors" value="{{ value }}" class="form-control" placeholder="Ex: Emma Goldmann">
|
||||||
|
{% when None %}
|
||||||
|
<input type="text" name="authors" class="form-control" placeholder="Ex: Emma Goldmann">
|
||||||
|
{% endmatch %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="owner_id" class="form-label">Owner</label>
|
||||||
|
|
||||||
|
<select name="owner_id" class="form-control">
|
||||||
|
<option></option>
|
||||||
|
{% match query.owner_id %}
|
||||||
|
{% when Some with (owner_id) %}
|
||||||
|
{% for user in users %}
|
||||||
|
{% if *owner_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="col-md-2">
|
||||||
|
<label for="current_holder_id" class="form-label">Current Holder</label>
|
||||||
|
|
||||||
|
<select name="current_holder_id" class="form-control">
|
||||||
|
<option></option>
|
||||||
|
{% match query.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="col-md-1 d-flex align-items-end">
|
||||||
|
<input type="submit" value="Search" class="btn btn-info btn">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
{% call cards::card() %}
|
{% call cards::card() %}
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
@ -19,14 +94,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for book_with_user in books_with_user %}
|
{% for book_user in books_with_user %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{{ book_with_user.book.id }}</th>
|
<th scope="row">{{ book_user.book.id }}</th>
|
||||||
<td>{{ book_with_user.book.title }}</td>
|
<td>{{ book_user.book.title }}</td>
|
||||||
<td>{{ book_with_user.book.authors }}</td>
|
<td>{{ book_user.book.authors }}</td>
|
||||||
<td>{{ book_with_user.owner.name }}</td>
|
<td>{{ book_user.owner.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% match book_with_user.current_holder %}
|
{% match book_user.current_holder %}
|
||||||
{% when Some with (current_holder) %}
|
{% when Some with (current_holder) %}
|
||||||
{{ current_holder.name }}
|
{{ current_holder.name }}
|
||||||
{% when None %}
|
{% when None %}
|
||||||
@ -34,7 +109,7 @@
|
|||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ dropdown::book_dropdown_button(book_with_user.book) }}
|
{{ dropdown::book_dropdown_button(book_user.book) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -45,19 +120,19 @@
|
|||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
<li class="page-item {% if current_page <= 1 %}disabled{% endif %}">
|
<li class="page-item {% if current_page <= 1 %}disabled{% endif %}">
|
||||||
<a class="page-link" href="/?page={% if current_page > 1 %}{{ current_page - 1 }}{% else %}1{% endif %}">Prev</a>
|
<a class="page-link" href="/?{{ base_query }}page={% if current_page > 1 %}{{ current_page - 1 }}{% else %}1{% endif %}">Prev</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% for page in 1..(total_page + 1) %}
|
{% for page in 1..(total_page + 1) %}
|
||||||
{% if page >= current_page - 1 && page <= current_page + 1 %}
|
{% if page >= current_page - 1 && page <= current_page + 1 %}
|
||||||
<li class="page-item {% if page == current_page %}active{% endif %}">
|
<li class="page-item {% if page == current_page %}active{% endif %}">
|
||||||
<a class="page-link" href="/?page={{ page }}">{{ page }}</a>
|
<a class="page-link" href="/?{{ base_query }}page={{ page }}">{{ page }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<li class="page-item {% if current_page == total_page %}disabled{% endif %}">
|
<li class="page-item {% if current_page == total_page %}disabled{% endif %}">
|
||||||
<a class="page-link" href="/?page={{ current_page + 1 }}">Next</a>
|
<a class="page-link" href="/?{{ base_query }}page={{ current_page + 1 }}">Next</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user