From 50a7af2b06fbac8df2ee4bb1feccdcaa4538ad58 Mon Sep 17 00:00:00 2001 From: Alphonse Paix Date: Fri, 3 Oct 2025 21:12:17 +0200 Subject: [PATCH] Comments management --- src/routes/admin/dashboard.rs | 17 +++- src/routes/comments.rs | 106 ++++++++++++++++++++----- src/routes/posts.rs | 10 +-- src/startup.rs | 2 + src/templates.rs | 12 +++ templates/dashboard/comments/card.html | 47 +++++++++++ templates/dashboard/comments/list.html | 59 ++++++++++++++ 7 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 templates/dashboard/comments/card.html diff --git a/src/routes/admin/dashboard.rs b/src/routes/admin/dashboard.rs index 413c24e..370d24a 100644 --- a/src/routes/admin/dashboard.rs +++ b/src/routes/admin/dashboard.rs @@ -1,4 +1,7 @@ -use crate::routes::{POSTS_PER_PAGE, SUBS_PER_PAGE, get_posts_count, get_posts_page, get_users}; +use crate::routes::{ + COMMENTS_PER_PAGE, POSTS_PER_PAGE, SUBS_PER_PAGE, get_comments_count, get_comments_page, + get_posts_count, get_posts_page, get_users, +}; use crate::{ authentication::AuthenticatedUser, routes::{AppError, get_max_page, get_subs, get_total_subs}, @@ -57,6 +60,14 @@ pub async fn admin_dashboard( .await .context("Could not fetch posts count.")?; let posts_max_page = get_max_page(posts_count, POSTS_PER_PAGE); + let comments_current_page = 1; + let comments = get_comments_page(&connection_pool, comments_current_page) + .await + .context("Could not fetch comments.")?; + let comments_count = get_comments_count(&connection_pool) + .await + .context("Could not fetch comments count.")?; + let comments_max_page = get_max_page(comments_count, COMMENTS_PER_PAGE); let template = DashboardTemplate { user, idempotency_key_1, @@ -71,6 +82,10 @@ pub async fn admin_dashboard( posts_current_page, posts_max_page, posts_count, + comments, + comments_current_page, + comments_max_page, + comments_count, }; Ok(Html(template.render().unwrap()).into_response()) } diff --git a/src/routes/comments.rs b/src/routes/comments.rs index fb2c5e4..1946c5d 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -1,3 +1,5 @@ +use crate::routes::get_max_page; +use crate::templates::CommentsPageDashboardTemplate; use crate::{ domain::CommentEntry, routes::AppError, @@ -5,6 +7,7 @@ use crate::{ templates::{CommentsList, HtmlTemplate, MessageTemplate}, }; use anyhow::Context; +use askama::Template; use axum::{ Form, extract::{Path, Query, State}, @@ -77,7 +80,7 @@ async fn insert_comment( Ok(comment_id) } -const COMMENTS_PER_PAGE: i64 = 5; +pub const COMMENTS_PER_PAGE: i64 = 5; #[derive(serde::Deserialize)] pub struct GetCommentsQueryParams { @@ -92,13 +95,13 @@ pub async fn get_comments( connection_pool, .. }): State, ) -> Result { - let comments = fetch_comments_page(&connection_pool, post_id, page) + let comments = get_comments_page_for_post(&connection_pool, post_id, page) .await .context("Could not fetch comments.")?; - let count = fetch_comments_count(&connection_pool, post_id) + let count = get_comments_count_for_post(&connection_pool, post_id) .await .context("Could not fetch comments count")?; - let max_page = get_comments_page_count(count); + let max_page = get_max_page(count, COMMENTS_PER_PAGE); let template = HtmlTemplate(CommentsList { comments, current_page: page, @@ -107,7 +110,50 @@ pub async fn get_comments( Ok(template.into_response()) } -pub async fn fetch_comments_page( +#[tracing::instrument(name = "Fetching all comments", skip(connection_pool))] +pub async fn get_all_comments( + Query(GetCommentsQueryParams { page }): Query, + State(AppState { + connection_pool, .. + }): State, +) -> Result { + let comments = get_comments_page(&connection_pool, page) + .await + .context("Could not fetch comments.")?; + let count = get_comments_count(&connection_pool) + .await + .context("Could not fetch comments count")?; + let comments_max_page = get_max_page(count, COMMENTS_PER_PAGE); + let template = HtmlTemplate(CommentsPageDashboardTemplate { + comments, + comments_current_page: page, + comments_max_page, + }); + Ok(template.into_response()) +} + +pub async fn delete_comment( + State(AppState { + connection_pool, .. + }): State, + crate::routes::Path(comment_id): crate::routes::Path, +) -> Result { + let res = sqlx::query!("DELETE FROM comments WHERE comment_id = $1", comment_id) + .execute(&connection_pool) + .await + .context("Failed to delete comment from database.") + .map_err(AppError::unexpected_message)?; + if res.rows_affected() > 1 { + Err(AppError::unexpected_message(anyhow::anyhow!( + "We could not find the comment in the database." + ))) + } else { + let template = MessageTemplate::success("The comment has been deleted.".into()); + Ok(template.render().unwrap().into_response()) + } +} + +pub async fn get_comments_page_for_post( connection_pool: &PgPool, post_id: Uuid, page: i64, @@ -116,13 +162,13 @@ pub async fn fetch_comments_page( let comments = sqlx::query_as!( CommentEntry, " - SELECT comment_id, post_id, author, content, published_at - FROM comments - WHERE post_id = $1 - ORDER BY published_at DESC - LIMIT $2 - OFFSET $3 - ", + SELECT comment_id, post_id, author, content, published_at + FROM comments + WHERE post_id = $1 + ORDER BY published_at DESC + LIMIT $2 + OFFSET $3 + ", post_id, COMMENTS_PER_PAGE, offset @@ -132,7 +178,7 @@ pub async fn fetch_comments_page( Ok(comments) } -pub async fn fetch_comments_count( +pub async fn get_comments_count_for_post( connection_pool: &PgPool, post_id: Uuid, ) -> Result { @@ -143,10 +189,32 @@ pub async fn fetch_comments_count( Ok(count) } -pub fn get_comments_page_count(count: i64) -> i64 { - let mut max_page = count.div_euclid(COMMENTS_PER_PAGE); - if count % COMMENTS_PER_PAGE > 0 { - max_page += 1; - } - max_page +pub async fn get_comments_page( + connection_pool: &PgPool, + page: i64, +) -> Result, sqlx::Error> { + let offset = (page - 1) * COMMENTS_PER_PAGE; + let comments = sqlx::query_as!( + CommentEntry, + " + SELECT comment_id, post_id, author, content, published_at + FROM comments + ORDER BY published_at DESC + LIMIT $1 + OFFSET $2 + ", + COMMENTS_PER_PAGE, + offset + ) + .fetch_all(connection_pool) + .await?; + Ok(comments) +} + +pub async fn get_comments_count(connection_pool: &PgPool) -> Result { + let count = sqlx::query_scalar!("SELECT count(*) FROM comments") + .fetch_one(connection_pool) + .await? + .unwrap_or(0); + Ok(count) } diff --git a/src/routes/posts.rs b/src/routes/posts.rs index b0b497a..42ae4d8 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -1,9 +1,9 @@ -use crate::routes::get_max_page; +use crate::routes::{COMMENTS_PER_PAGE, get_max_page}; use crate::templates::PostsPageDashboardTemplate; use crate::{ domain::PostEntry, routes::{ - AppError, Path, Query, fetch_comments_count, fetch_comments_page, get_comments_page_count, + AppError, Path, Query, get_comments_count_for_post, get_comments_page_for_post, not_found_html, }, startup::AppState, @@ -144,11 +144,11 @@ pub async fn see_post( .to_html() .context("Could not render markdown with extension.")?; let current_page = 1; - let comments_count = fetch_comments_count(&connection_pool, post_id) + let comments_count = get_comments_count_for_post(&connection_pool, post_id) .await .context("Could not fetch comment count")?; - let max_page = get_comments_page_count(comments_count); - let comments = fetch_comments_page(&connection_pool, post_id, 1) + let max_page = get_max_page(comments_count, COMMENTS_PER_PAGE); + let comments = get_comments_page_for_post(&connection_pool, post_id, 1) .await .context("Failed to fetch latest comments")?; let template = HtmlTemplate(PostTemplate { diff --git a/src/startup.rs b/src/startup.rs index 66a6693..9b378ab 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -91,6 +91,8 @@ pub fn app( .route("/posts/{post_id}", delete(delete_post)) .route("/users", post(create_user)) .route("/users/{user_id}", delete(delete_user)) + .route("/comments", get(get_all_comments)) + .route("/comments/{comment_id}", delete(delete_comment)) .layer(middleware::from_fn(require_admin)); let auth_routes = Router::new() .route("/dashboard", get(admin_dashboard)) diff --git a/src/templates.rs b/src/templates.rs index d1604aa..4416861 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -70,6 +70,10 @@ pub struct DashboardTemplate { pub posts_current_page: i64, pub posts_max_page: i64, pub posts_count: i64, + pub comments: Vec, + pub comments_current_page: i64, + pub comments_max_page: i64, + pub comments_count: i64, } #[derive(Template)] @@ -80,6 +84,14 @@ pub struct PostsPageDashboardTemplate { pub posts_max_page: i64, } +#[derive(Template)] +#[template(path = "dashboard/comments/list.html", block = "comments")] +pub struct CommentsPageDashboardTemplate { + pub comments: Vec, + pub comments_current_page: i64, + pub comments_max_page: i64, +} + #[derive(Template)] #[template(path = "home.html")] pub struct HomeTemplate; diff --git a/templates/dashboard/comments/card.html b/templates/dashboard/comments/card.html new file mode 100644 index 0000000..d425e6b --- /dev/null +++ b/templates/dashboard/comments/card.html @@ -0,0 +1,47 @@ +
+
+
+
+ + {% if let Some(name) = comment.author %} + {{ name }} + {% else %} + Anonymous + {% endif %} + + + on + + #{{ comment.post_id.to_string()[..8] }} + +
+

{{ comment.content }}

+
+ + + + +
+
+ +
+
\ No newline at end of file diff --git a/templates/dashboard/comments/list.html b/templates/dashboard/comments/list.html index e69de29..554cb71 100644 --- a/templates/dashboard/comments/list.html +++ b/templates/dashboard/comments/list.html @@ -0,0 +1,59 @@ +
+
+
+
+

+ + + + Comments management ({{ comments_count }}) +

+

View and moderate all comments.

+
+
+
+
+ {% block comments %} + {% if comments.is_empty() %} +
+
+ + + +
+

No data to display

+

The request did not return any data.

+
+ {% else %} +
+ {% for comment in comments %} + {% include "dashboard/comments/card.html" %} + {% endfor %} +
+ {% endif %} +
+ + Page: {{ comments_current_page }} + +
+ {% endblock %} +
+
\ No newline at end of file