Files
zero2prod/src/routes/posts.rs
Alphonse Paix 4cb1d2b6fd
Some checks failed
Rust / Test (push) Has been cancelled
Rust / Rustfmt (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Code coverage (push) Has been cancelled
Compute dashboard stats
Track open rate for new post notifications (user clicked the button in
the link or not). No data about the user is collected during the
process, it only uses an ID inserted by the issue delivery worker.
2025-09-24 04:30:27 +02:00

143 lines
3.9 KiB
Rust

use crate::{
domain::PostEntry,
routes::AppError,
startup::AppState,
templates::{PostListTemplate, PostTemplate, PostsTemplate},
};
use anyhow::Context;
use askama::Template;
use axum::{
extract::{Path, Query, State},
response::{Html, IntoResponse, Redirect, Response},
};
use sqlx::PgPool;
use uuid::Uuid;
const NUM_PER_PAGE: i64 = 3;
pub async fn list_posts(
State(AppState {
connection_pool, ..
}): State<AppState>,
) -> Result<Response, AppError> {
let count = get_posts_table_size(&connection_pool)
.await
.context("Could not fetch posts table size.")
.map_err(AppError::unexpected_page)?;
let next_page = if count > NUM_PER_PAGE { Some(2) } else { None };
let posts = get_posts(&connection_pool, NUM_PER_PAGE, None)
.await
.context("Could not fetch latest posts")
.map_err(AppError::unexpected_page)?;
let template = PostsTemplate { posts, next_page };
Ok(Html(template.render().unwrap()).into_response())
}
async fn get_posts(
connection_pool: &PgPool,
n: i64,
offset: Option<i64>,
) -> Result<Vec<PostEntry>, sqlx::Error> {
sqlx::query_as!(
PostEntry,
r#"
SELECT p.post_id, u.username AS author, p.title, p.content, p.published_at
FROM posts p
LEFT JOIN users u ON p.author_id = u.user_id
ORDER BY p.published_at DESC
LIMIT $1
OFFSET $2
"#,
n,
offset
)
.fetch_all(connection_pool)
.await
}
async fn get_posts_table_size(connection_pool: &PgPool) -> Result<i64, sqlx::Error> {
sqlx::query!("SELECT count(*) FROM posts")
.fetch_one(connection_pool)
.await
.map(|r| r.count.unwrap())
}
#[derive(serde::Deserialize)]
pub struct PostParams {
origin: Option<Uuid>,
}
pub async fn see_post(
State(AppState {
connection_pool, ..
}): State<AppState>,
Path(post_id): Path<Uuid>,
Query(PostParams { origin }): Query<PostParams>,
) -> Result<Response, AppError> {
if let Some(origin) = origin {
sqlx::query!(
"UPDATE notifications_delivered SET opened = TRUE WHERE email_id = $1",
origin,
)
.execute(&connection_pool)
.await
.context("Failed to mark email as opened.")
.map_err(AppError::unexpected_page)?;
return Ok(Redirect::to(&format!("/posts/{}", post_id)).into_response());
}
let post = get_post_data(&connection_pool, post_id)
.await
.context(format!("Failed to fetch post #{}", post_id))
.map_err(AppError::unexpected_page)?
.to_html()
.context("Could not render markdown with extension.")?;
let template = PostTemplate { post };
Ok(Html(template.render().unwrap()).into_response())
}
async fn get_post_data(connection_pool: &PgPool, post_id: Uuid) -> Result<PostEntry, sqlx::Error> {
sqlx::query_as!(
PostEntry,
r#"
SELECT p.post_id, u.username AS author, p.title, p.content, p.published_at
FROM posts p
LEFT JOIN users u ON p.author_id = u.user_id
WHERE p.post_id = $1
"#,
post_id
)
.fetch_one(connection_pool)
.await
}
#[derive(serde::Deserialize)]
pub struct LoadMoreParams {
page: i64,
}
pub async fn load_more(
State(AppState {
connection_pool, ..
}): State<AppState>,
Query(LoadMoreParams { page }): Query<LoadMoreParams>,
) -> Result<Response, AppError> {
let offset = (page - 1) * NUM_PER_PAGE;
let posts = get_posts(&connection_pool, NUM_PER_PAGE, Some(offset))
.await
.context("Could not fetch posts from database.")?;
let count = posts.len();
Ok(Html(
PostListTemplate {
posts,
next_page: if count as i64 == NUM_PER_PAGE {
Some(page + 1)
} else {
None
},
}
.render()
.unwrap(),
)
.into_response())
}