Support for comments
This commit is contained in:
152
src/routes/comments.rs
Normal file
152
src/routes/comments.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use crate::{
|
||||
domain::CommentEntry,
|
||||
routes::AppError,
|
||||
startup::AppState,
|
||||
templates::{CommentsList, HtmlTemplate, MessageTemplate},
|
||||
};
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
Form,
|
||||
extract::{Path, Query, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CommentPathParam {
|
||||
post_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct CommentForm {
|
||||
pub author: Option<String>,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Posting new comment", skip_all, fields(post_id = %post_id))]
|
||||
pub async fn post_comment(
|
||||
Path(CommentPathParam { post_id }): Path<CommentPathParam>,
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
Form(form): Form<CommentForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
validate_form(&form)?;
|
||||
let comment_id = insert_comment(&connection_pool, post_id, form)
|
||||
.await
|
||||
.context("Could not insert comment into database.")?;
|
||||
tracing::info!("new comment with id {} has been inserted", comment_id);
|
||||
let template = HtmlTemplate(MessageTemplate::success(
|
||||
"Your comment has been posted.".into(),
|
||||
));
|
||||
Ok(template.into_response())
|
||||
}
|
||||
|
||||
fn validate_form(form: &CommentForm) -> Result<(), anyhow::Error> {
|
||||
if form.content.is_empty() {
|
||||
anyhow::bail!("Comment content cannot be empty.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Inserting new comment in database", skip_all, fields(comment_id = tracing::field::Empty))]
|
||||
async fn insert_comment(
|
||||
connection_pool: &PgPool,
|
||||
post_id: Uuid,
|
||||
form: CommentForm,
|
||||
) -> Result<Uuid, sqlx::Error> {
|
||||
let author = form
|
||||
.author
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().to_string());
|
||||
let comment_id = Uuid::new_v4();
|
||||
tracing::Span::current().record("comment_id", comment_id.to_string());
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO comments (comment_id, post_id, author, content)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
",
|
||||
comment_id,
|
||||
post_id,
|
||||
author,
|
||||
form.content.trim()
|
||||
)
|
||||
.execute(connection_pool)
|
||||
.await?;
|
||||
Ok(comment_id)
|
||||
}
|
||||
|
||||
const COMMENTS_PER_PAGE: i64 = 5;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct GetCommentsQueryParams {
|
||||
page: i64,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Fetching comments", skip(connection_pool))]
|
||||
pub async fn get_comments(
|
||||
Path(CommentPathParam { post_id }): Path<CommentPathParam>,
|
||||
Query(GetCommentsQueryParams { page }): Query<GetCommentsQueryParams>,
|
||||
State(AppState {
|
||||
connection_pool, ..
|
||||
}): State<AppState>,
|
||||
) -> Result<Response, AppError> {
|
||||
let comments = fetch_comments_page(&connection_pool, post_id, page)
|
||||
.await
|
||||
.context("Could not fetch comments.")?;
|
||||
let count = fetch_comments_count(&connection_pool, post_id)
|
||||
.await
|
||||
.context("Could not fetch comments count")?;
|
||||
let max_page = get_comments_page_count(count);
|
||||
let template = HtmlTemplate(CommentsList {
|
||||
comments,
|
||||
current_page: page,
|
||||
max_page,
|
||||
});
|
||||
Ok(template.into_response())
|
||||
}
|
||||
|
||||
pub async fn fetch_comments_page(
|
||||
connection_pool: &PgPool,
|
||||
post_id: Uuid,
|
||||
page: i64,
|
||||
) -> Result<Vec<CommentEntry>, 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
|
||||
WHERE post_id = $1
|
||||
ORDER BY published_at DESC
|
||||
LIMIT $2
|
||||
OFFSET $3
|
||||
",
|
||||
post_id,
|
||||
COMMENTS_PER_PAGE,
|
||||
offset
|
||||
)
|
||||
.fetch_all(connection_pool)
|
||||
.await?;
|
||||
Ok(comments)
|
||||
}
|
||||
|
||||
pub async fn fetch_comments_count(
|
||||
connection_pool: &PgPool,
|
||||
post_id: Uuid,
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
let count = sqlx::query_scalar!("SELECT count(*) FROM comments WHERE post_id = $1", post_id)
|
||||
.fetch_one(connection_pool)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
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
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::{
|
||||
domain::PostEntry,
|
||||
routes::{AppError, Path, Query, not_found_html},
|
||||
routes::{
|
||||
AppError, Path, Query, fetch_comments_count, fetch_comments_page, get_comments_page_count,
|
||||
not_found_html,
|
||||
},
|
||||
startup::AppState,
|
||||
templates::{HtmlTemplate, PostListTemplate, PostTemplate, PostsTemplate},
|
||||
};
|
||||
@@ -89,7 +92,21 @@ pub async fn see_post(
|
||||
let post = post
|
||||
.to_html()
|
||||
.context("Could not render markdown with extension.")?;
|
||||
let template = HtmlTemplate(PostTemplate { post });
|
||||
let current_page = 1;
|
||||
let comments_count = fetch_comments_count(&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)
|
||||
.await
|
||||
.context("Failed to fetch latest comments")?;
|
||||
let template = HtmlTemplate(PostTemplate {
|
||||
post,
|
||||
comments,
|
||||
current_page,
|
||||
max_page,
|
||||
comments_count,
|
||||
});
|
||||
Ok(template.into_response())
|
||||
} else {
|
||||
Ok(not_found_html())
|
||||
|
||||
Reference in New Issue
Block a user