feat: add tests for the forum threads and use sqlx::query_as!() more

often
This commit is contained in:
FrenchGithubUser
2025-12-13 13:39:21 +01:00
parent a9337c462b
commit 6e4e15f9dd
20 changed files with 1716 additions and 159 deletions

View File

@@ -0,0 +1,4 @@
INSERT INTO
forum_categories (id, name, created_by_id)
VALUES
(100, 'Test Category', 100);

View File

@@ -0,0 +1,7 @@
INSERT INTO
forum_posts (id, forum_thread_id, content, created_at, created_by_id)
VALUES
(100, 100, 'This is the first post in the test thread', '2025-01-01 10:00:00+00', 100),
(101, 101, 'This is the first post in the locked thread', '2025-01-01 11:00:00+00', 100),
(102, 102, 'This is the first post in the sticky thread', '2025-01-01 12:00:00+00', 100),
(103, 103, 'This is the first post in the thread in different sub category', '2025-01-01 13:00:00+00', 100);

View File

@@ -0,0 +1,5 @@
INSERT INTO
forum_sub_categories (id, forum_category_id, name, created_by_id)
VALUES
(100, 100, 'Test Sub Category', 100),
(101, 100, 'Test Sub Category 2', 100);

View File

@@ -0,0 +1,7 @@
INSERT INTO
forum_threads (id, forum_sub_category_id, name, created_at, created_by_id, posts_amount, sticky, locked)
VALUES
(100, 100, 'Test Thread', '2025-01-01 10:00:00+00', 100, 1, false, false),
(101, 100, 'Locked Thread', '2025-01-01 11:00:00+00', 100, 1, false, true),
(102, 100, 'Sticky Thread', '2025-01-01 12:00:00+00', 100, 1, true, false),
(103, 101, 'Thread in Different Sub Category', '2025-01-01 13:00:00+00', 100, 1, false, false);

View File

@@ -1,4 +1,4 @@
INSERT INTO
users (username, email, password_hash, registered_from_ip, passkey, class)
users (id, username, email, password_hash, registered_from_ip, passkey, class)
VALUES
('test_user', 'test_email@testdomain.com', '$argon2id$v=19$m=19456,t=2,p=1$WM6V9pJ2ya7+N+NNIUtolg$n128u9idizCHLwZ9xhKaxOttLaAVZZgvfRZlRAnfyKk', '10.10.4.88', 'd2037c66dd3e13044e0d2f9b891c3837', 'newbie')
(100, 'test_user', 'test_email@testdomain.com', '$argon2id$v=19$m=19456,t=2,p=1$WM6V9pJ2ya7+N+NNIUtolg$n128u9idizCHLwZ9xhKaxOttLaAVZZgvfRZlRAnfyKk', '10.10.4.88', 'd2037c66dd3e13044e0d2f9b891c3837', 'newbie')

View File

@@ -1,4 +1,4 @@
INSERT INTO
users (username, email, password_hash, registered_from_ip, passkey, class)
users (id, username, email, password_hash, registered_from_ip, passkey, class)
VALUES
('test_user2', 'test_email2@testdomain.com', '$argon2id$v=19$m=19456,t=2,p=1$WM6V9pJ2ya7+N+NNIUtolg$n128u9idizCHLwZ9xhKaxOttLaAVZZgvfRZlRAnfyKk', '10.10.4.88', 'd2037c66dd3e13044e0d2f9b891c3838', 'staff')
(101, 'test_user2', 'test_email2@testdomain.com', '$argon2id$v=19$m=19456,t=2,p=1$WM6V9pJ2ya7+N+NNIUtolg$n128u9idizCHLwZ9xhKaxOttLaAVZZgvfRZlRAnfyKk', '10.10.4.88', 'd2037c66dd3e13044e0d2f9b891c3838', 'staff')

File diff suppressed because it is too large Load Diff

View File

@@ -152,7 +152,7 @@ async fn test_upload_torrent(pool: PgPool) {
.await;
assert_eq!(torrent.edition_group_id, 1);
assert_eq!(torrent.created_by_id, 2);
assert_eq!(torrent.created_by_id, 100);
}
#[sqlx::test(

View File

@@ -231,6 +231,15 @@ pub enum Error {
#[error("could not update forum thread")]
CouldNotUpdateForumThread(#[source] sqlx::Error),
#[error("forum thread locked")]
ForumThreadLocked,
#[error("forum thread name cannot be empty")]
ForumThreadNameEmpty,
#[error("forum post empty")]
ForumPostEmpty,
#[error("could not find forum post")]
CouldNotFindForumPost(#[source] sqlx::Error),
@@ -347,7 +356,9 @@ impl actix_web::ResponseError for Error {
| Error::InvitationKeyAlreadyUsed
| Error::WrongUsernameOrPassword
| Error::TorrentFileInvalid
| Error::InvalidUserIdOrTorrentId => StatusCode::BAD_REQUEST,
| Error::InvalidUserIdOrTorrentId
| Error::ForumThreadNameEmpty
| Error::ForumPostEmpty => StatusCode::BAD_REQUEST,
// 401 Unauthorized
Error::InvalidOrExpiredRefreshToken | Error::InvalidatedToken => {
@@ -355,7 +366,9 @@ impl actix_web::ResponseError for Error {
}
// 403 Forbidden
Error::AccountBanned | Error::InsufficientPrivileges => StatusCode::FORBIDDEN,
Error::AccountBanned | Error::InsufficientPrivileges | Error::ForumThreadLocked => {
StatusCode::FORBIDDEN
}
// 404 Not Found
Error::UserNotFound(_)
@@ -365,6 +378,8 @@ impl actix_web::ResponseError for Error {
| Error::CouldNotFindArtist(_)
| Error::TitleGroupTagNotFound
| Error::CouldNotFindTitleGroupComment(_)
| Error::CouldNotFindForumThread(_)
| Error::CouldNotFindForumSubCategory(_)
| Error::CssSheetNotFound(_) => StatusCode::NOT_FOUND,
// 409 Conflict

View File

@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM forum_threads WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "forum_sub_category_id",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "created_by_id",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "posts_amount",
"type_info": "Int8"
},
{
"ordinal": 6,
"name": "sticky",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "locked",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "168361f135ba2ea642ce12377d2a08f84b9a4baa3e196f5c0a8e4cf40a7a8dac"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n json_strip_nulls(\n json_build_object(\n 'id', fsc.id,\n 'name', fsc.name,\n 'threads_amount', fsc.threads_amount,\n 'posts_amount', fsc.posts_amount,\n 'forbidden_classes', fsc.forbidden_classes,\n 'category', json_build_object(\n 'id', fc.id,\n 'name', fc.name\n ),\n 'threads', (\n SELECT\n COALESCE(\n json_agg(\n json_build_object(\n 'id', ft.id,\n 'name', ft.name,\n 'created_at', ft.created_at,\n 'posts_amount', ft.posts_amount,\n 'sticky', ft.sticky,\n 'locked', ft.locked,\n 'created_by', json_build_object(\n 'id', u_thread.id,\n 'username', u_thread.username,\n 'warned', u_thread.warned,\n 'banned', u_thread.banned\n ),\n 'latest_post', json_build_object(\n 'id', fp_latest.id,\n 'thread_id', ft.id,\n 'name', ft.name,\n 'created_at', fp_latest.created_at,\n 'created_by', json_build_object(\n 'id', u_post.id,\n 'username', u_post.username,\n 'warned', u_post.warned,\n 'banned', u_post.banned\n )\n )\n ) ORDER BY ft.created_at DESC\n ),\n '[]'::json\n )\n FROM\n forum_threads ft\n JOIN\n users u_thread ON ft.created_by_id = u_thread.id\n LEFT JOIN LATERAL (\n SELECT\n fp.id,\n fp.created_at,\n fp.created_by_id\n FROM\n forum_posts fp\n WHERE\n fp.forum_thread_id = ft.id\n ORDER BY\n fp.created_at DESC\n LIMIT 1\n ) AS fp_latest ON TRUE\n LEFT JOIN\n users u_post ON fp_latest.created_by_id = u_post.id\n WHERE\n ft.forum_sub_category_id = fsc.id\n )\n )\n ) AS result_json\n FROM\n forum_sub_categories fsc\n JOIN\n forum_categories fc ON fsc.forum_category_id = fc.id\n WHERE\n fsc.id = $1\n GROUP BY\n fsc.id, fc.id;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result_json",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
null
]
},
"hash": "54ae7811a6cb02d3b9161fc6e5e5c419f0a7b891033dd0c047be2532c07b0b80"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT locked FROM forum_threads WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "locked",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "61907f8f65cb6ef293e8f5ec9bf0248f1e9b81574e5fdc11ca5fb0471b28110c"
}

View File

@@ -0,0 +1,104 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT fsc.id, fsc.name, fsc.threads_amount, fsc.posts_amount, fsc.forbidden_classes,\n fsc.forum_category_id, fc.name AS category_name,\n fp.id AS \"latest_post_id?\", ft.id AS \"thread_id?\", ft.name AS \"thread_name?\", fp.created_at AS \"latest_post_created_at?\",\n u.id AS \"user_id?\", u.username AS \"username?\", u.warned AS \"warned?\", u.banned AS \"banned?\"\n FROM forum_sub_categories fsc\n INNER JOIN forum_categories fc ON fsc.forum_category_id = fc.id\n LEFT JOIN LATERAL (\n SELECT fp.id, fp.created_at, fp.created_by_id, fp.forum_thread_id\n FROM forum_posts fp\n JOIN forum_threads ft_inner ON fp.forum_thread_id = ft_inner.id\n WHERE ft_inner.forum_sub_category_id = fsc.id\n ORDER BY fp.created_at DESC LIMIT 1\n ) AS fp ON TRUE\n LEFT JOIN forum_threads ft ON fp.forum_thread_id = ft.id\n LEFT JOIN users u ON fp.created_by_id = u.id\n ORDER BY fsc.forum_category_id, fsc.name\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "threads_amount",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "posts_amount",
"type_info": "Int8"
},
{
"ordinal": 4,
"name": "forbidden_classes",
"type_info": "VarcharArray"
},
{
"ordinal": 5,
"name": "forum_category_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "category_name",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "latest_post_id?",
"type_info": "Int8"
},
{
"ordinal": 8,
"name": "thread_id?",
"type_info": "Int8"
},
{
"ordinal": 9,
"name": "thread_name?",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "latest_post_created_at?",
"type_info": "Timestamptz"
},
{
"ordinal": 11,
"name": "user_id?",
"type_info": "Int4"
},
{
"ordinal": 12,
"name": "username?",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "warned?",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "banned?",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "9df37ddfafd7cee54e187d4b8195d00f67fb73156ba8a7a0cc23ebc062fa6a71"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n json_strip_nulls(\n json_build_object(\n 'id', fsc.id,\n 'name', fsc.name,\n 'threads_amount', fsc.threads_amount,\n 'posts_amount', fsc.posts_amount,\n 'forbidden_classes', fsc.forbidden_classes,\n 'category', json_build_object(\n 'id', fc.id,\n 'name', fc.name\n ),\n 'threads', (\n SELECT\n COALESCE(\n json_agg(\n json_build_object(\n 'id', ft.id,\n 'name', ft.name,\n 'created_at', ft.created_at,\n 'posts_amount', ft.posts_amount,\n 'latest_post', CASE\n WHEN fp_latest.id IS NOT NULL THEN json_build_object(\n 'id', fp_latest.id,\n 'created_at', fp_latest.created_at,\n 'created_by', json_build_object(\n 'id', u_post.id,\n 'username', u_post.username\n )\n )\n ELSE NULL\n END\n ) ORDER BY ft.created_at DESC\n ),\n '[]'::json\n )\n FROM\n forum_threads ft\n LEFT JOIN LATERAL (\n SELECT\n fp.id,\n fp.created_at,\n fp.created_by_id\n FROM\n forum_posts fp\n WHERE\n fp.forum_thread_id = ft.id\n ORDER BY\n fp.created_at DESC\n LIMIT 1\n ) AS fp_latest ON TRUE\n LEFT JOIN\n users u_post ON fp_latest.created_by_id = u_post.id\n WHERE\n ft.forum_sub_category_id = fsc.id\n )\n )\n ) AS result_json\n FROM\n forum_sub_categories fsc\n JOIN\n forum_categories fc ON fsc.forum_category_id = fc.id\n WHERE\n fsc.id = $1\n GROUP BY\n fsc.id, fc.id;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "result_json",
"type_info": "Json"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
null
]
},
"hash": "bcad9fe9afe358b71ba0084ea3b19c361479b45fd3937d2a2ac636748cb4cfcd"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, name FROM forum_categories ORDER BY id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "c79d160078a506e5568530d1134001dc5c1c43b6c98e30c64c889ce186712ca9"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n json_build_object(\n 'forum_categories', json_agg(\n json_build_object(\n 'id', fc.id,\n 'name', fc.name,\n 'sub_categories', (\n SELECT\n json_agg(\n json_build_object(\n 'id', fsc.id,\n 'name', fsc.name,\n 'threads_amount', fsc.threads_amount,\n 'posts_amount', fsc.posts_amount,\n 'forbidden_classes', '[]'::jsonb,\n 'latest_post_in_thread', CASE\n WHEN ft.id IS NOT NULL THEN json_build_object(\n 'id', ft.id,\n 'name', ft.name,\n 'created_at', ft.latest_post_created_at,\n 'created_by', json_build_object( -- Changed to a JSON object for user details\n 'id', ft.latest_post_created_by_id,\n 'username', ft.latest_post_created_by_username\n ),\n 'posts_amount', ft.posts_amount\n )\n ELSE NULL\n END\n ) ORDER BY fsc.name\n )\n FROM\n forum_sub_categories fsc\n LEFT JOIN LATERAL (\n SELECT\n ft_with_latest_post.id,\n ft_with_latest_post.name,\n ft_with_latest_post.posts_amount,\n fp_latest.created_at AS latest_post_created_at,\n fp_latest.created_by_id AS latest_post_created_by_id,\n u.username AS latest_post_created_by_username -- Joined to get the username\n FROM\n forum_posts fp_latest\n JOIN\n forum_threads ft_with_latest_post ON fp_latest.forum_thread_id = ft_with_latest_post.id\n JOIN\n users u ON fp_latest.created_by_id = u.id -- Joined with the users table\n WHERE\n ft_with_latest_post.forum_sub_category_id = fsc.id\n ORDER BY\n fp_latest.created_at DESC\n LIMIT 1\n ) AS ft ON TRUE\n WHERE\n fsc.forum_category_id = fc.id\n )\n ) ORDER BY fc.id\n )\n ) AS forum_overview\n FROM\n forum_categories fc;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "forum_overview",
"type_info": "Json"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "d0d5add41466b664ae7a33540e81e774ed0a48e05e8842363e1cf7b28de52668"
}

View File

@@ -110,7 +110,7 @@ pub struct ForumSubCategoryHierarchy {
pub threads_amount: i64,
pub posts_amount: i64,
pub forbidden_classes: Vec<String>,
pub latest_post_in_thread: ForumThreadPostLite,
pub latest_post_in_thread: Option<ForumThreadPostLite>,
pub threads: Option<Vec<ForumThreadHierarchy>>,
pub category: ForumCategoryLite,
}
@@ -131,6 +131,7 @@ pub struct ForumThreadHierarchy {
#[derive(Debug, Deserialize, Serialize, FromRow, ToSchema)]
pub struct ForumThreadPostLite {
pub id: i64,
pub thread_id: i64,
pub name: String,
#[schema(value_type = String, format = DateTime)]
pub created_at: DateTime<Local>,

View File

@@ -3,30 +3,82 @@ use crate::{
models::{
common::PaginatedResults,
forum::{
EditedForumPost, EditedForumThread, ForumPost, ForumPostAndThreadName,
ForumPostHierarchy, ForumSearchQuery, ForumSearchResult, ForumThread,
ForumThreadEnriched, GetForumThreadPostsQuery, UserCreatedForumPost,
EditedForumPost, EditedForumThread, ForumCategoryHierarchy, ForumCategoryLite,
ForumPost, ForumPostAndThreadName, ForumPostHierarchy, ForumSearchQuery,
ForumSearchResult, ForumSubCategoryHierarchy, ForumThread, ForumThreadEnriched,
ForumThreadPostLite, GetForumThreadPostsQuery, UserCreatedForumPost,
UserCreatedForumThread,
},
user::UserLiteAvatar,
user::{UserLite, UserLiteAvatar},
},
};
use arcadia_common::error::{Error, Result};
use chrono::{DateTime, Utc};
use serde_json::{json, Value};
use chrono::{DateTime, Local, Utc};
use serde_json::Value;
use sqlx::{prelude::FromRow, PgPool};
use std::borrow::Borrow;
#[derive(FromRow)]
struct DBImportSubCategoryWithLatestPost {
id: i32,
name: String,
threads_amount: i64,
posts_amount: i64,
forbidden_classes: Vec<String>,
forum_category_id: i32,
category_name: String,
latest_post_id: Option<i64>,
thread_id: Option<i64>,
thread_name: Option<String>,
latest_post_created_at: Option<DateTime<Utc>>,
user_id: Option<i32>,
username: Option<String>,
warned: Option<bool>,
banned: Option<bool>,
}
#[derive(Debug, FromRow)]
struct DBImportForumPost {
id: i64,
content: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
sticky: bool,
locked: bool,
forum_thread_id: i64,
created_by_user_id: i32,
created_by_user_username: String,
created_by_user_avatar: Option<String>,
created_by_user_banned: bool,
created_by_user_warned: bool,
}
impl ConnectionPool {
pub async fn create_forum_post(
&self,
forum_post: &UserCreatedForumPost,
current_user_id: i32,
) -> Result<ForumPost> {
if forum_post.content.trim().is_empty() {
return Err(Error::ForumPostEmpty);
}
let mut tx = <ConnectionPool as Borrow<PgPool>>::borrow(self)
.begin()
.await?;
let thread = sqlx::query!(
r#"SELECT locked FROM forum_threads WHERE id = $1"#,
forum_post.forum_thread_id
)
.fetch_one(&mut *tx)
.await
.map_err(Error::CouldNotCreateForumPost)?;
if thread.locked {
return Err(Error::ForumThreadLocked);
}
let created_forum_post = sqlx::query_as!(
ForumPost,
r#"
@@ -120,6 +172,14 @@ impl ConnectionPool {
forum_thread: &mut UserCreatedForumThread,
current_user_id: i32,
) -> Result<ForumThread> {
if forum_thread.name.trim().is_empty() {
return Err(Error::ForumThreadNameEmpty);
}
if forum_thread.first_post.content.trim().is_empty() {
return Err(Error::ForumPostEmpty);
}
let mut tx = <ConnectionPool as Borrow<PgPool>>::borrow(self)
.begin()
.await?;
@@ -155,11 +215,21 @@ impl ConnectionPool {
tx.commit().await?;
// TODO: include this in the transaction
// Create the first post (this will increment posts_amount)
self.create_forum_post(&forum_thread.first_post, current_user_id)
.await?;
Ok(created_forum_thread)
// Fetch and return the updated thread with correct posts_amount
let updated_thread = sqlx::query_as!(
ForumThread,
r#"SELECT * FROM forum_threads WHERE id = $1"#,
created_forum_thread.id
)
.fetch_one(self.borrow())
.await
.map_err(Error::CouldNotFindForumThread)?;
Ok(updated_thread)
}
pub async fn update_forum_thread(
@@ -167,6 +237,10 @@ impl ConnectionPool {
edited_thread: &EditedForumThread,
user_id: i32,
) -> Result<ForumThreadEnriched> {
if edited_thread.name.trim().is_empty() {
return Err(Error::BadRequest("Thread name cannot be empty".to_string()));
}
let updated_thread = sqlx::query_as!(
ForumThreadEnriched,
r#"
@@ -212,81 +286,109 @@ impl ConnectionPool {
Ok(updated_thread)
}
pub async fn find_forum_cateogries_hierarchy(&self) -> Result<Value> {
let forum_overview = sqlx::query!(
pub async fn find_forum_cateogries_hierarchy(&self) -> Result<Vec<ForumCategoryHierarchy>> {
// Query all categories at once
let categories = sqlx::query_as!(
ForumCategoryLite,
"SELECT id, name FROM forum_categories ORDER BY id"
)
.fetch_all(self.borrow())
.await
.map_err(Error::CouldNotFindForumSubCategory)?;
// Query all subcategories with their latest posts in one query
let sub_categories_data = sqlx::query_as!(
DBImportSubCategoryWithLatestPost,
r#"
SELECT
json_build_object(
'forum_categories', json_agg(
json_build_object(
'id', fc.id,
'name', fc.name,
'sub_categories', (
SELECT
json_agg(
json_build_object(
'id', fsc.id,
'name', fsc.name,
'threads_amount', fsc.threads_amount,
'posts_amount', fsc.posts_amount,
'forbidden_classes', '[]'::jsonb,
'latest_post_in_thread', CASE
WHEN ft.id IS NOT NULL THEN json_build_object(
'id', ft.id,
'name', ft.name,
'created_at', ft.latest_post_created_at,
'created_by', json_build_object( -- Changed to a JSON object for user details
'id', ft.latest_post_created_by_id,
'username', ft.latest_post_created_by_username
),
'posts_amount', ft.posts_amount
)
ELSE NULL
END
) ORDER BY fsc.name
)
FROM
forum_sub_categories fsc
LEFT JOIN LATERAL (
SELECT
ft_with_latest_post.id,
ft_with_latest_post.name,
ft_with_latest_post.posts_amount,
fp_latest.created_at AS latest_post_created_at,
fp_latest.created_by_id AS latest_post_created_by_id,
u.username AS latest_post_created_by_username -- Joined to get the username
FROM
forum_posts fp_latest
JOIN
forum_threads ft_with_latest_post ON fp_latest.forum_thread_id = ft_with_latest_post.id
JOIN
users u ON fp_latest.created_by_id = u.id -- Joined with the users table
WHERE
ft_with_latest_post.forum_sub_category_id = fsc.id
ORDER BY
fp_latest.created_at DESC
LIMIT 1
) AS ft ON TRUE
WHERE
fsc.forum_category_id = fc.id
)
) ORDER BY fc.id
)
) AS forum_overview
FROM
forum_categories fc;
SELECT fsc.id, fsc.name, fsc.threads_amount, fsc.posts_amount, fsc.forbidden_classes,
fsc.forum_category_id, fc.name AS category_name,
fp.id AS "latest_post_id?", ft.id AS "thread_id?", ft.name AS "thread_name?", fp.created_at AS "latest_post_created_at?",
u.id AS "user_id?", u.username AS "username?", u.warned AS "warned?", u.banned AS "banned?"
FROM forum_sub_categories fsc
INNER JOIN forum_categories fc ON fsc.forum_category_id = fc.id
LEFT JOIN LATERAL (
SELECT fp.id, fp.created_at, fp.created_by_id, fp.forum_thread_id
FROM forum_posts fp
JOIN forum_threads ft_inner ON fp.forum_thread_id = ft_inner.id
WHERE ft_inner.forum_sub_category_id = fsc.id
ORDER BY fp.created_at DESC LIMIT 1
) AS fp ON TRUE
LEFT JOIN forum_threads ft ON fp.forum_thread_id = ft.id
LEFT JOIN users u ON fp.created_by_id = u.id
ORDER BY fsc.forum_category_id, fsc.name
"#
)
.fetch_one(self.borrow())
.fetch_all(self.borrow())
.await
.expect("error getting forums");
.map_err(Error::CouldNotFindForumSubCategory)?;
Ok(forum_overview
.forum_overview
.unwrap()
.get("forum_categories")
.unwrap_or(&json!([]))
.to_owned())
// Build hierarchy by grouping subcategories by category
use std::collections::HashMap;
let mut category_map: HashMap<i32, Vec<ForumSubCategoryHierarchy>> = HashMap::new();
for sc in sub_categories_data {
let sub_category = ForumSubCategoryHierarchy {
id: sc.id,
name: sc.name,
threads_amount: sc.threads_amount,
posts_amount: sc.posts_amount,
forbidden_classes: sc.forbidden_classes,
latest_post_in_thread: match (
sc.latest_post_id,
sc.thread_id,
sc.thread_name,
sc.latest_post_created_at,
sc.user_id,
sc.username,
sc.warned,
sc.banned,
) {
(
Some(id),
Some(thread_id),
Some(name),
Some(created_at),
Some(user_id),
Some(username),
Some(warned),
Some(banned),
) => Some(ForumThreadPostLite {
id,
thread_id,
name,
created_at: created_at.with_timezone(&Local),
created_by: UserLite {
id: user_id,
username,
warned,
banned,
},
}),
_ => None,
},
threads: None,
category: ForumCategoryLite {
id: sc.forum_category_id,
name: sc.category_name,
},
};
category_map
.entry(sc.forum_category_id)
.or_default()
.push(sub_category);
}
// Build final result with categories in order
let forum_categories = categories
.into_iter()
.map(|category| ForumCategoryHierarchy {
id: category.id,
name: category.name,
sub_categories: category_map.remove(&category.id).unwrap_or_default(),
})
.collect();
Ok(forum_categories)
}
pub async fn find_forum_sub_category_threads(
@@ -316,23 +418,34 @@ impl ConnectionPool {
'name', ft.name,
'created_at', ft.created_at,
'posts_amount', ft.posts_amount,
'latest_post', CASE
WHEN fp_latest.id IS NOT NULL THEN json_build_object(
'id', fp_latest.id,
'created_at', fp_latest.created_at,
'created_by', json_build_object(
'id', u_post.id,
'username', u_post.username
)
'sticky', ft.sticky,
'locked', ft.locked,
'created_by', json_build_object(
'id', u_thread.id,
'username', u_thread.username,
'warned', u_thread.warned,
'banned', u_thread.banned
),
'latest_post', json_build_object(
'id', fp_latest.id,
'thread_id', ft.id,
'name', ft.name,
'created_at', fp_latest.created_at,
'created_by', json_build_object(
'id', u_post.id,
'username', u_post.username,
'warned', u_post.warned,
'banned', u_post.banned
)
ELSE NULL
END
)
) ORDER BY ft.created_at DESC
),
'[]'::json
)
FROM
forum_threads ft
JOIN
users u_thread ON ft.created_by_id = u_thread.id
LEFT JOIN LATERAL (
SELECT
fp.id,
@@ -364,12 +477,16 @@ impl ConnectionPool {
"#,
forum_sub_category_id
)
.fetch_one(self.borrow())
.fetch_optional(self.borrow())
.await
.map_err(Error::CouldNotFindForumSubCategory)?;
//TODO: unwrap can fail return Error::CouldNotFindForumSubCategory
Ok(forum_sub_category.result_json.unwrap())
match forum_sub_category {
Some(record) => Ok(record.result_json.unwrap_or(serde_json::json!({}))),
None => Err(Error::CouldNotFindForumSubCategory(
sqlx::Error::RowNotFound,
)),
}
}
pub async fn find_forum_thread(
@@ -447,22 +564,6 @@ impl ConnectionPool {
((form.page.unwrap_or(1) - 1) as i64) * page_size
};
#[derive(Debug, FromRow)]
struct DBImportForumPost {
id: i64,
content: String,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
sticky: bool,
locked: bool,
forum_thread_id: i64,
created_by_user_id: i32,
created_by_user_username: String,
created_by_user_avatar: Option<String>,
created_by_user_banned: bool,
created_by_user_warned: bool,
}
let posts = sqlx::query_as!(
DBImportForumPost,
r#"

View File

@@ -11,7 +11,7 @@
</Column>
<Column style="width: 35%" field="latest_post_in_thread.name" :header="t('forum.latest_post')">
<template #body="slotProps">
<RouterLink :to="'/forum/thread/' + slotProps.data.latest_post_in_thread.id">
<RouterLink :to="'/forum/thread/' + slotProps.data.latest_post_in_thread.thread_id">
{{ slotProps.data.latest_post_in_thread.name }}
</RouterLink>
</template>

View File

@@ -613,7 +613,7 @@ export interface ForumSubCategoryHierarchy {
'category': ForumCategoryLite;
'forbidden_classes': Array<string>;
'id': number;
'latest_post_in_thread': ForumThreadPostLite;
'latest_post_in_thread'?: ForumThreadPostLite | null;
'name': string;
'posts_amount': number;
'threads'?: Array<ForumThreadHierarchy> | null;
@@ -658,6 +658,7 @@ export interface ForumThreadPostLite {
'created_by': UserLite;
'id': number;
'name': string;
'thread_id': number;
}
export interface GetForumThreadPostsQuery {
'page'?: number | null;