Add edit and new for users

This commit is contained in:
gabatxo1312 2026-01-29 23:52:55 +01:00
parent 57af399ace
commit cf9a7cf33f
No known key found for this signature in database
12 changed files with 229 additions and 88 deletions

View File

@ -23,8 +23,11 @@ pub fn build_app(state: AppState) -> Router {
.route("/books/{id}/edit", get(routes::book::edit))
.route("/books/new", get(routes::book::new))
.route("/users", get(routes::user::index))
.route("/users/new", get(routes::user::new))
.route("/users/{id}/edit", get(routes::user::edit))
.route("/users/{id}", post(routes::user::update))
.route("/users", post(routes::user::create))
.route("/users/{id}", post(routes::user::delete))
.route("/users/{id}/delete", post(routes::user::delete))
.nest("/assets", static_router())
.with_state(state)
}

View File

@ -108,7 +108,7 @@ impl BookOperator {
let book_pages = Entity::find()
.filter(conditions)
.order_by_desc(Column::Id)
.paginate(&self.state.db, 1);
.paginate(&self.state.db, 100);
let books = book_pages
.fetch_page(page_0indexed)
@ -140,6 +140,15 @@ impl BookOperator {
}
}
/// Finds vec of book by its Owner
pub async fn find_all_by_owner(&self, owner_id: i32) -> Result<Vec<Model>, BookError> {
Entity::find()
.filter(Column::OwnerId.eq(owner_id))
.all(&self.state.db)
.await
.context(DBSnafu)
}
/// Creates a new book from the given form data.
pub async fn create(&self, form: BookForm) -> Result<Model, BookError> {
let book = ActiveModel {

View File

@ -1,5 +1,7 @@
use crate::models::book::BookOperator;
use crate::routes::user::UserForm;
use crate::state::AppState;
use crate::state::error::UserSnafu;
use sea_orm::ActiveValue::Set;
use sea_orm::DeleteResult;
use sea_orm::entity::prelude::*;
@ -74,6 +76,20 @@ impl UserOperator {
user.insert(&self.state.db).await.context(DBSnafu)
}
pub async fn update(&self, id: i32, form: UserForm) -> Result<Model, UserError> {
let user_by_id = Self::find_by_id(&self, id).await.context(UserSnafu);
if let Ok(user) = user_by_id {
let mut user: ActiveModel = user.into();
user.name = Set(form.name);
user.update(&self.state.db).await.context(DBSnafu)
} else {
Err(UserError::NotFound { id })
}
}
pub async fn delete(&self, user_id: i32) -> Result<DeleteResult, UserError> {
let user: Option<Model> = Entity::find_by_id(user_id)
.one(&self.state.db)

View File

@ -24,7 +24,16 @@ use crate::{
#[derive(Template, WebTemplate)]
#[template(path = "users/index.html")]
struct UsersIndexTemplate {
user_with_books_number: Vec<(user::Model, usize, usize)>,
user_with_books_number: Vec<UserWithBookNumber>,
}
pub struct UserWithBookNumber {
/// the user model
pub user: user::Model,
/// the number of books owned by this user
pub owner_book_number: usize,
/// the number of books borrowed by this user
pub borrowed_book_number: usize,
}
pub async fn index(
@ -40,7 +49,7 @@ pub async fn index(
.await
.context(BookSnafu)?;
let mut result: Vec<(user::Model, usize, usize)> = vec![];
let mut result: Vec<UserWithBookNumber> = Vec::with_capacity(users.len());
let mut owner_books: HashMap<i32, usize> = HashMap::new();
let mut borrowed_books: HashMap<i32, usize> = HashMap::new();
@ -56,7 +65,11 @@ pub async fn index(
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));
result.push(UserWithBookNumber {
user,
owner_book_number: *owner_books_size,
borrowed_book_number: *borrowed_books_size,
});
}
Ok(UsersIndexTemplate {
@ -73,7 +86,7 @@ pub async fn create(
State(state): State<AppState>,
Form(form): Form<UserForm>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let _user = UserOperator::new(state)
let _ = UserOperator::new(state)
.create(form)
.await
.context(UserSnafu)?;
@ -81,6 +94,19 @@ pub async fn create(
Ok(Redirect::to("/users"))
}
pub async fn update(
State(state): State<AppState>,
Path(id): Path<i32>,
Form(form): Form<UserForm>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let _ = UserOperator::new(state)
.update(id, form)
.await
.context(UserSnafu)?;
Ok(Redirect::to("/users"))
}
pub async fn delete(
State(state): State<AppState>,
Path(id): Path<i32>,
@ -92,3 +118,29 @@ pub async fn delete(
Ok(Redirect::to("/users"))
}
#[derive(Template, WebTemplate)]
#[template(path = "users/edit.html")]
struct EditTemplate {
user: user::Model,
}
pub async fn edit(
State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<impl axum::response::IntoResponse, AppStateError> {
let user = UserOperator::new(state)
.find_by_id(id)
.await
.context(UserSnafu)?;
Ok(EditTemplate { user })
}
#[derive(Template, WebTemplate)]
#[template(path = "users/new.html")]
struct NewTemplate {}
pub async fn new() -> impl axum::response::IntoResponse {
NewTemplate {}
}

View File

@ -1,50 +1,27 @@
{% extends "base.html" %}
{% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
{% import "components/inputs.html" as form_helpers %}
{% block main %}
{{ typography::heading("New book") }}
{% call cards::card() %}
<form method="post" action="/books">
<div class="mb-3">
<label for="title" class="form-label">Name</label>
<input type="text" name="title" class="form-control" required>
</div>
{{ form_helpers::input("title", "Name", is_required = true, placeholder = "Ex: La Petite Dernière") }}
{{ form_helpers::input("authors", "Author(s)", is_required = true, placeholder = "Ex: Fatima Daas") }}
<div class="mb-3">
<label for="authors" class="form-label">Author(s)</label>
<input type="text" name="authors" class="form-control" required>
</div>
{% call(option) form_helpers::select("owner_id", "Owner", users, is_required = true) %}
<option value="{{ option.id }}">{{ option.name }}</option>
{% endcall %}
<div class="mb-3">
<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>
{% call(option) form_helpers::select("current_holder_id", "Current Holder", users, is_required = false) %}
<option value="{{ option.id }}">{{ option.name }}</option>
{% endcall %}
<div class="mb-3">
<label for="current_holder_id" class="form-label">Current Holder</label>
<select name="current_holder_id" class="form-control">
<option></option>
{% for user in users %}
<option value="{{ user.id }}">{{ user.name }}</option>
{% endfor %}
</select>
</div>
{{ form_helpers::textarea("description", "Description", rows = 5, is_required = false, placeholder = "Ex: 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.") }}
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea name="description" class="form-control"></textarea>
</div>
<div class="mb-3">
<label for="comment" class="form-label">Comment</label>
<textarea name="comment" class="form-control"></textarea>
</div>
{{ form_helpers::textarea("comment", "Comment", rows = 3, is_required = false, placeholder = "Ex: I recommend it, it's great!") }}
<div class="mt-4 text-center">
<input type="submit" value="Create book" class="btn btn-success">

View File

@ -11,18 +11,18 @@
</div>
{% endmacro %}
{% macro book_dropdown_button(book, show = true) %}
{% macro crud_dropdown_button(book, label, sub_path, 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>
<li><a class="dropdown-item" href="/{{ sub_path }}/{{ book.id }}">Show</a></li>
{% endif %}
<li><a class="dropdown-item" href="/books/{{ book.id }}/edit">Edit</a></li>
<li><a class="dropdown-item" href="/{{ sub_path }}/{{ book.id }}/edit">Edit</a></li>
<li>
<form method="post" action="/books/{{ book.id }}/delete">
<form method="post" action="/{{ sub_path }}/{{ book.id }}/delete" class="mb-0">
<input class="dropdown-item" type="submit" value="Delete">
</form>
</li>

View File

@ -0,0 +1,43 @@
{% macro input(name, label, value = "", type = "text", is_required = false, placeholder = "", margin_bottom = true) %}
<div {% if margin_bottom %}class="mb-3"{% endif %}>
<label for="{{ name }}" class="form-label">
{{ label }}
{% if is_required %}
<span class="text-danger">*</span>
{% endif %}
</label>
<input type="{{ type }}" value="{{ value }}" name="{{ name }}" class="form-control" placeholder="{{ placeholder }}" {% if is_required %}required{% endif %}>
</div>
{% endmacro %}
{% macro select(name, label, options, selected_value = "", is_required = false, margin_bottom = true) -%}
<div {% if margin_bottom %}class="mb-3"{% endif %}>
<label for="{{ name }}" class="form-label">
{{ label }}
{% if is_required %}
<span class="text-danger">*</span>
{% endif %}
</label>
<select name="{{ name }}" class="form-select" {% if is_required %}required{% endif %}>
{% if !is_required %}
<option></option>
{% endif %}
{% for option in options %}
{{ caller(option) }}
{% endfor %}
</select>
</div>
{%- endmacro %}
{% macro textarea(name, label, value = "", rows = 2, is_required = false, placeholder = "", margin_bottom = true) %}
<div {% if margin_bottom %}class="mb-3"{% endif %}>
<label for="{{ name }}" class="form-label">
{{ label }}
{% if is_required %}
<span class="text-danger">*</span>
{% endif %}
</label>
<textarea value="{{ value }}" name="{{ name }}" rows="{{ rows }}" class="form-control" placeholder="{{ placeholder }}" {% if is_required %}required{% endif %}></textarea>
</div>
{% endmacro %}

View File

@ -21,7 +21,7 @@
</h1>
<div>
{{ dropdown::book_dropdown_button(book, show) }}
{{ dropdown::crud_dropdown_button(book, "Actions", "books", show) }}
</div>
</div>
{% endmacro %}

View File

@ -33,7 +33,7 @@
<div class="col-md-2">
<label for="owner_id" class="form-label">Owner</label>
<select name="owner_id" class="form-control">
<select name="owner_id" class="form-select">
<option></option>
{% match query.owner_id %}
{% when Some with (owner_id) %}
@ -55,7 +55,7 @@
<div class="col-md-2">
<label for="current_holder_id" class="form-label">Current Holder</label>
<select name="current_holder_id" class="form-control">
<select name="current_holder_id" class="form-select">
<option></option>
{% match query.current_holder_id %}
{% when Some with (current_holder_id) %}
@ -75,7 +75,11 @@
</div>
<div class="col-md-1 d-flex align-items-end">
<input type="submit" value="Search" class="btn btn-info btn">
<input type="submit" value="Search" class="btn btn-info w-100">
</div>
<div class="col-md-1 d-flex align-items-end">
<a href="/" class="btn btn-light">Reset</a>
</div>
</div>
</form>
@ -95,7 +99,7 @@
</thead>
<tbody>
{% for book_user in books_with_user %}
<tr>
<tr class="align-middle">
<th scope="row">{{ book_user.book.id }}</th>
<td>{{ book_user.book.title }}</td>
<td>{{ book_user.book.authors }}</td>
@ -109,7 +113,7 @@
{% endmatch %}
</td>
<td>
{{ dropdown::book_dropdown_button(book_user.book) }}
{{ dropdown::crud_dropdown_button(book_user.book, "Actions", "books") }}
</td>
</tr>
{% endfor %}
@ -117,25 +121,27 @@
</table>
{% if total_page > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item {% if current_page <= 1 %}disabled{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={% if current_page > 1 %}{{ current_page - 1 }}{% else %}1{% endif %}">Prev</a>
</li>
<div class="d-flex justify-content-center mt-1">
<nav aria-label="Page navigation">
<ul class="pagination">
<li class="page-item {% if current_page <= 1 %}disabled{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={% if current_page > 1 %}{{ current_page - 1 }}{% else %}1{% endif %}">Prev</a>
</li>
{% for page in 1..(total_page + 1) %}
{% if page >= current_page - 1 && page <= current_page + 1 %}
<li class="page-item {% if page == current_page %}active{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={{ page }}">{{ page }}</a>
</li>
{% endif %}
{% endfor %}
{% for page in 1..(total_page + 1) %}
{% if page >= current_page - 1 && page <= current_page + 1 %}
<li class="page-item {% if page == current_page %}active{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={{ page }}">{{ page }}</a>
</li>
{% endif %}
{% endfor %}
<li class="page-item {% if current_page == total_page %}disabled{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={{ current_page + 1 }}">Next</a>
</li>
</ul>
</nav>
<li class="page-item {% if current_page == total_page %}disabled{% endif %}">
<a class="page-link" href="/?{{ base_query }}page={{ current_page + 1 }}">Next</a>
</li>
</ul>
</nav>
</div>
{% endif %}
{% endcall %}
{% endblock %}

22
templates/users/edit.html Normal file
View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
o{% import "components/inputs.html" as form_helpers %}
{% block main %}
{{ typography::heading("New User") }}
{% call cards::card() %}
<form action="/users/{{ user.id }}" method="post">
<div class="row align-items-end">
<div class="col-md-10">
{{ form_helpers::input("name", "Name", value = user.name, is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }}
</div>
<div class="col-md-2">
<input type="submit" value="Create user" class="btn btn-success">
</div>
</div>
</form>
{% endcall %}
{% endblock %}

View File

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% import "components/typography.html" as typography %}
{% import "components/dropdown.html" as dropdown %}
{% import "components/cards.html" as cards %}
{% import "components/inputs.html" as form_helpers %}
{% block main %}
{{ typography::heading("All users") }}
{% call typography::heading("All users") %}
<a class="btn-success btn" href="/users/new">Add User</a>
{% endcall %}
{% call cards::card() %}
<table class="table table-hover">
@ -17,30 +21,18 @@
</tr>
</thead>
<tbody>
{% for (user, book_size, borrowed_book) in user_with_books_number %}
{% for user_information in user_with_books_number %}
<tr>
<th scope="row">{{ user.id }}</th>
<td>{{ user.name }}</td>
<td>{{ book_size }}</td>
<td>{{ borrowed_book }}</td>
<th scope="row">{{ user_information.user.id }}</th>
<td>{{ user_information.user.name }}</td>
<td>{{ user_information.owner_book_number }}</td>
<td>{{ user_information.borrowed_book_number }}</td>
<td>
<form method="post" action="/users/{{ user.id }}">
<input value="Delete" type="submit" class="btn btn-danger btn-sm">
</form>
{{ dropdown::crud_dropdown_button(user_information.user, "Actions", "users", show = false) }}
</td>
</tr>
{% 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 %}

21
templates/users/new.html Normal file
View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% import "components/typography.html" as typography %}
{% import "components/cards.html" as cards %}
o{% import "components/inputs.html" as form_helpers %}
{% block main %}
{{ typography::heading("New User") }}
{% call cards::card() %}
<form action="/users" method="post">
<div class="row align-items-end">
<div class="col-md-10">
{{ form_helpers::input("name", "Name", is_required = true, placeholder = "Ex: Kropotkine", margin_bottom = false) }}
</div>
<div class="col-md-2">
<input type="submit" value="Create user" class="btn btn-success">
</div>
</div>
</form>
{% endcall %}
{% endblock %}