diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index 2d9b0df6e5..c4403fca05 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -422,6 +422,25 @@ END; $$); +CALL r.create_triggers ('community_report', $$ +BEGIN + UPDATE + community_aggregates AS a + SET + report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count + FROM ( + SELECT + (community_report).community_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (community_report).resolved), 0) AS unresolved_report_count + FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (community_report).community_id) AS diff +WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0) + AND a.community_id = diff.community_id; + +RETURN NULL; + +END; + +$$); + -- These triggers create and update rows in each aggregates table to match its associated table's rows. -- Deleting rows and updating IDs are already handled by `CASCADE` in foreign key constraints. CREATE FUNCTION r.comment_aggregates_from_comment () @@ -685,3 +704,5 @@ CALL r.create_report_combined_trigger ('comment_report'); CALL r.create_report_combined_trigger ('private_message_report'); +CALL r.create_report_combined_trigger ('community_report'); + diff --git a/crates/db_schema/src/aggregates/structs.rs b/crates/db_schema/src/aggregates/structs.rs index 7a97666aab..a254b0a638 100644 --- a/crates/db_schema/src/aggregates/structs.rs +++ b/crates/db_schema/src/aggregates/structs.rs @@ -73,6 +73,8 @@ pub struct CommunityAggregates { #[serde(skip)] pub hot_rank: f64, pub subscribers_local: i64, + pub report_count: i16, + pub unresolved_report_count: i16, } #[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)] diff --git a/crates/db_schema/src/impls/community_report.rs b/crates/db_schema/src/impls/community_report.rs new file mode 100644 index 0000000000..85c3cc5c0d --- /dev/null +++ b/crates/db_schema/src/impls/community_report.rs @@ -0,0 +1,97 @@ +use crate::{ + newtypes::{CommunityId, CommunityReportId, PersonId}, + schema::community_report::{ + community_id, + dsl::{community_report, resolved, resolver_id, updated}, + }, + source::community_report::{CommunityReport, CommunityReportForm}, + traits::Reportable, + utils::{get_conn, DbPool}, +}; +use chrono::Utc; +use diesel::{ + dsl::{insert_into, update}, + result::Error, + ExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Reportable for CommunityReport { + type Form = CommunityReportForm; + type IdType = CommunityReportId; + type ObjectIdType = CommunityId; + /// creates a community report and returns it + /// + /// * `conn` - the postgres connection + /// * `community_report_form` - the filled CommunityReportForm to insert + async fn report( + pool: &mut DbPool<'_>, + community_report_form: &CommunityReportForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(community_report) + .values(community_report_form) + .get_result::(conn) + .await + } + + /// resolve a community report + /// + /// * `conn` - the postgres connection + /// * `report_id` - the id of the report to resolve + /// * `by_resolver_id` - the id of the user resolving the report + async fn resolve( + pool: &mut DbPool<'_>, + report_id_: Self::IdType, + by_resolver_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + update(community_report.find(report_id_)) + .set(( + resolved.eq(true), + resolver_id.eq(by_resolver_id), + updated.eq(Utc::now()), + )) + .execute(conn) + .await + } + + async fn resolve_all_for_object( + pool: &mut DbPool<'_>, + community_id_: CommunityId, + by_resolver_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + update(community_report.filter(community_id.eq(community_id_))) + .set(( + resolved.eq(true), + resolver_id.eq(by_resolver_id), + updated.eq(Utc::now()), + )) + .execute(conn) + .await + } + + /// unresolve a community report + /// + /// * `conn` - the postgres connection + /// * `report_id` - the id of the report to unresolve + /// * `by_resolver_id` - the id of the user unresolving the report + async fn unresolve( + pool: &mut DbPool<'_>, + report_id_: Self::IdType, + by_resolver_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + update(community_report.find(report_id_)) + .set(( + resolved.eq(false), + resolver_id.eq(by_resolver_id), + updated.eq(Utc::now()), + )) + .execute(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 2d7a16c2c6..37e16c2549 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -6,6 +6,7 @@ pub mod comment_reply; pub mod comment_report; pub mod community; pub mod community_block; +pub mod community_report; pub mod custom_emoji; pub mod email_verification; pub mod federation_allowlist; diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 4c4a9b66c1..85337b4b35 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -91,6 +91,12 @@ pub struct PersonMentionId(i32); /// The comment report id. pub struct CommentReportId(pub i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The community report id. +pub struct CommunityReportId(pub i32); + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 64aff118b5..a77b7f3e6f 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -253,6 +253,8 @@ diesel::table! { users_active_half_year -> Int8, hot_rank -> Float8, subscribers_local -> Int8, + report_count -> Int2, + unresolved_report_count -> Int2, } } @@ -263,6 +265,25 @@ diesel::table! { } } +diesel::table! { + community_report (id) { + id -> Int4, + creator_id -> Int4, + community_id -> Int4, + original_community_name -> Text, + original_community_title -> Text, + original_community_description -> Nullable, + original_community_sidebar -> Nullable, + original_community_icon -> Nullable, + original_community_banner -> Nullable, + reason -> Text, + resolved -> Bool, + resolver_id -> Nullable, + published -> Timestamptz, + updated -> Nullable, + } +} + diesel::table! { custom_emoji (id) { id -> Int4, @@ -896,6 +917,7 @@ diesel::table! { post_report_id -> Nullable, comment_report_id -> Nullable, private_message_report_id -> Nullable, + community_report_id -> Nullable, } } @@ -1014,6 +1036,7 @@ diesel::joinable!(community_actions -> community (community_id)); diesel::joinable!(community_aggregates -> community (community_id)); diesel::joinable!(community_language -> community (community_id)); diesel::joinable!(community_language -> language (language_id)); +diesel::joinable!(community_report -> community (community_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); @@ -1068,6 +1091,7 @@ diesel::joinable!(private_message_report -> private_message (private_message_id) diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); diesel::joinable!(report_combined -> comment_report (comment_report_id)); +diesel::joinable!(report_combined -> community_report (community_report_id)); diesel::joinable!(report_combined -> post_report (post_report_id)); diesel::joinable!(report_combined -> private_message_report (private_message_report_id)); diesel::joinable!(site -> instance (instance_id)); @@ -1093,6 +1117,7 @@ diesel::allow_tables_to_appear_in_same_query!( community_actions, community_aggregates, community_language, + community_report, custom_emoji, custom_emoji_keyword, email_verification, diff --git a/crates/db_schema/src/source/community_report.rs b/crates/db_schema/src/source/community_report.rs new file mode 100644 index 0000000000..57d863acc8 --- /dev/null +++ b/crates/db_schema/src/source/community_report.rs @@ -0,0 +1,60 @@ +use crate::newtypes::{CommunityId, CommunityReportId, DbUrl, PersonId}; +#[cfg(feature = "full")] +use crate::schema::community_report; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Queryable, Selectable, Associations, Identifiable, TS) +)] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::community::Community)) +)] +#[cfg_attr(feature = "full", diesel(table_name = community_report))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A comment report. +pub struct CommunityReport { + pub id: CommunityReportId, + pub creator_id: PersonId, + pub community_id: CommunityId, + pub original_community_name: String, + pub original_community_title: String, + #[cfg_attr(feature = "full", ts(optional))] + pub original_community_description: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub original_community_sidebar: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub original_community_icon: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub original_community_banner: Option, + pub reason: String, + pub resolved: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub resolver_id: Option, + pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] + pub updated: Option>, +} + +#[derive(Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = community_report))] +pub struct CommunityReportForm { + pub creator_id: PersonId, + pub community_id: CommunityId, + pub original_community_name: String, + pub original_community_title: String, + pub original_community_description: Option, + pub original_community_sidebar: Option, + pub original_community_icon: Option, + pub original_community_banner: Option, + pub reason: String, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 2ac2692b4c..2f6f9172b6 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -11,6 +11,7 @@ pub mod comment_reply; pub mod comment_report; pub mod community; pub mod community_block; +pub mod community_report; pub mod custom_emoji; pub mod custom_emoji_keyword; pub mod email_verification; diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs index 999681fe03..ab132f6261 100644 --- a/crates/db_views/src/report_combined_view.rs +++ b/crates/db_views/src/report_combined_view.rs @@ -1,5 +1,6 @@ use crate::structs::{ CommentReportView, + CommunityReportView, LocalUserView, PostReportView, PrivateMessageReportView, @@ -29,6 +30,8 @@ use lemmy_db_schema::{ comment_report, community, community_actions, + community_aggregates, + community_report, local_user, person, person_actions, @@ -64,6 +67,7 @@ impl ReportCombinedViewInternal { .left_join(post_report::table) .left_join(comment_report::table) .left_join(private_message_report::table) + .left_join(community_report::table) // Need to join to comment and post to get the community .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) // The post @@ -84,6 +88,7 @@ impl ReportCombinedViewInternal { post_report::resolved .or(comment_report::resolved) .or(private_message_report::resolved) + .or(community_report::resolved) .is_distinct_from(true), ) .into_boxed(); @@ -111,6 +116,7 @@ impl ReportCombinedPaginationCursor { ReportCombinedView::Comment(v) => ('C', v.comment_report.id.0), ReportCombinedView::Post(v) => ('P', v.post_report.id.0), ReportCombinedView::PrivateMessage(v) => ('M', v.private_message_report.id.0), + ReportCombinedView::Community(v) => ('Y', v.community_report.id.0), }; // hex encoding to prevent ossification ReportCombinedPaginationCursor(format!("{prefix}{id:x}")) @@ -127,6 +133,7 @@ impl ReportCombinedPaginationCursor { "C" => query.filter(report_combined::comment_report_id.eq(id)), "P" => query.filter(report_combined::post_report_id.eq(id)), "M" => query.filter(report_combined::private_message_report_id.eq(id)), + "Y" => query.filter(report_combined::community_report_id.eq(id)), _ => return Err(err_msg()), }; let token = query.first(&mut get_conn(pool).await?).await?; @@ -167,13 +174,15 @@ impl ReportCombinedQuery { .left_join(post_report::table) .left_join(comment_report::table) .left_join(private_message_report::table) + .left_join(community_report::table) // The report creator .inner_join( person::table.on( post_report::creator_id .eq(person::id) .or(comment_report::creator_id.eq(person::id)) - .or(private_message_report::creator_id.eq(person::id)), + .or(private_message_report::creator_id.eq(person::id)) + .or(community_report::creator_id.eq(person::id)), ), ) // The comment @@ -192,7 +201,7 @@ impl ReportCombinedQuery { ), ) // The item creator (`item_creator` is the id of this person) - .inner_join( + .left_join( aliases::person1.on( post::creator_id .eq(item_creator) @@ -201,7 +210,13 @@ impl ReportCombinedQuery { ), ) // The community - .left_join(community::table.on(post::community_id.eq(community::id))) + .left_join( + community::table.on( + post::community_id + .eq(community::id) + .or(community_report::community_id.eq(community::id)), + ), + ) .left_join(actions_alias( creator_community_actions, item_creator, @@ -217,7 +232,7 @@ impl ReportCombinedQuery { .left_join(actions( community_actions::table, Some(my_person_id), - post::community_id, + community::id, )) .left_join(actions(post_actions::table, Some(my_person_id), post::id)) .left_join(actions( @@ -229,13 +244,18 @@ impl ReportCombinedQuery { .left_join( comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), ) + .left_join( + community_aggregates::table + .on(community_report::community_id.eq(community_aggregates::community_id)), + ) // The resolver .left_join( aliases::person2.on( private_message_report::resolver_id .eq(resolver) .or(post_report::resolver_id.eq(resolver)) - .or(comment_report::resolver_id.eq(resolver)), + .or(comment_report::resolver_id.eq(resolver)) + .or(community_report::resolver_id.eq(resolver)), ), ) .left_join(actions( @@ -266,9 +286,12 @@ impl ReportCombinedQuery { // Private-message-specific private_message_report::all_columns.nullable(), private_message::all_columns.nullable(), + // Community-specific + community_report::all_columns.nullable(), + community_aggregates::all_columns.nullable(), // Shared person::all_columns, - aliases::person1.fields(person::all_columns), + aliases::person1.fields(person::all_columns.nullable()), community::all_columns.nullable(), CommunityFollower::select_subscribed_type(), aliases::person2.fields(person::all_columns.nullable()), @@ -286,12 +309,20 @@ impl ReportCombinedQuery { .into_boxed(); if let Some(community_id) = self.community_id { - query = query.filter(community::id.eq(community_id)); + query = query.filter( + community::id + .eq(community_id) + .and(report_combined::community_report_id.is_null()), + ); } // If its not an admin, get only the ones you mod if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); + query = query.filter( + community_actions::became_moderator + .is_not_null() + .and(report_combined::community_report_id.is_null()), + ); } let mut query = PaginatedQueryBuilder::new(query); @@ -312,6 +343,7 @@ impl ReportCombinedQuery { post_report::resolved .or(comment_report::resolved) .or(private_message_report::resolved) + .or(community_report::resolved) .is_distinct_from(true), ) // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, @@ -338,12 +370,20 @@ fn map_to_enum(view: ReportCombinedViewInternal) -> Option { // Use for a short alias let v = view; - if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( + if let ( + Some(post_report), + Some(post), + Some(community), + Some(unread_comments), + Some(counts), + Some(post_creator), + ) = ( v.post_report, v.post.clone(), v.community.clone(), v.post_unread_comments, v.post_counts, + v.item_creator.clone(), ) { Some(ReportCombinedView::Post(PostReportView { post_report, @@ -352,7 +392,7 @@ fn map_to_enum(view: ReportCombinedViewInternal) -> Option { unread_comments, counts, creator: v.report_creator, - post_creator: v.item_creator, + post_creator, creator_banned_from_community: v.item_creator_banned_from_community, creator_is_moderator: v.item_creator_is_moderator, creator_is_admin: v.item_creator_is_admin, @@ -364,12 +404,20 @@ fn map_to_enum(view: ReportCombinedViewInternal) -> Option { my_vote: v.my_post_vote, resolver: v.resolver, })) - } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( + } else if let ( + Some(comment_report), + Some(comment), + Some(counts), + Some(post), + Some(community), + Some(comment_creator), + ) = ( v.comment_report, v.comment, v.comment_counts, v.post.clone(), v.community.clone(), + v.item_creator.clone(), ) { Some(ReportCombinedView::Comment(CommentReportView { comment_report, @@ -378,7 +426,7 @@ fn map_to_enum(view: ReportCombinedViewInternal) -> Option { post, community, creator: v.report_creator, - comment_creator: v.item_creator, + comment_creator, creator_banned_from_community: v.item_creator_banned_from_community, creator_is_moderator: v.item_creator_is_moderator, creator_is_admin: v.item_creator_is_admin, @@ -388,18 +436,32 @@ fn map_to_enum(view: ReportCombinedViewInternal) -> Option { my_vote: v.my_comment_vote, resolver: v.resolver, })) - } else if let (Some(private_message_report), Some(private_message)) = - (v.private_message_report, v.private_message) + } else if let ( + Some(private_message_report), + Some(private_message), + Some(private_message_creator), + ) = (v.private_message_report, v.private_message, v.item_creator) { Some(ReportCombinedView::PrivateMessage( PrivateMessageReportView { private_message_report, private_message, creator: v.report_creator, - private_message_creator: v.item_creator, + private_message_creator, resolver: v.resolver, }, )) + } else if let (Some(community), Some(community_report), Some(counts)) = + (v.community, v.community_report, v.community_counts) + { + Some(ReportCombinedView::Community(CommunityReportView { + community_report, + community, + creator: v.report_creator, + counts, + subscribed: v.subscribed, + resolver: v.resolver, + })) } else { None } @@ -426,6 +488,7 @@ mod tests { comment::{Comment, CommentInsertForm}, comment_report::{CommentReport, CommentReportForm}, community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, + community_report::{CommunityReport, CommunityReportForm}, instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, local_user_vote_display_mode::LocalUserVoteDisplayMode, @@ -551,6 +614,20 @@ mod tests { let pool = &mut pool.into(); let data = init_data(pool).await?; + // Sara reports the community + let sara_report_community_form = CommunityReportForm { + creator_id: data.sara.id, + community_id: data.community.id, + original_community_name: data.community.name.clone(), + original_community_title: data.community.title.clone(), + original_community_banner: None, + original_community_description: None, + original_community_sidebar: None, + original_community_icon: None, + reason: "from sara".into(), + }; + CommunityReport::report(pool, &sara_report_community_form).await?; + // sara reports the post let sara_report_post_form = PostReportForm { creator_id: data.sara.id, @@ -592,9 +669,14 @@ mod tests { let reports = ReportCombinedQuery::default() .list(pool, &data.admin_view) .await?; - assert_eq!(3, reports.len()); + assert_eq!(4, reports.len()); // Make sure the report types are correct + if let ReportCombinedView::Community(v) = &reports[3] { + assert_eq!(data.community.id, v.community.id); + } else { + panic!("wrong type"); + } if let ReportCombinedView::Post(v) = &reports[2] { assert_eq!(data.post.id, v.post.id); assert_eq!(data.sara.id, v.creator.id); @@ -617,7 +699,7 @@ mod tests { let report_count_admin = ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?; - assert_eq!(3, report_count_admin); + assert_eq!(4, report_count_admin); // Timmy should only see 2 reports, since they're not an admin, // but they do mod the community @@ -964,4 +1046,62 @@ mod tests { Ok(()) } + + #[tokio::test] + #[serial] + async fn test_community_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // jessica reports community + let community_report_form = CommunityReportForm { + creator_id: data.jessica.id, + community_id: data.community.id, + original_community_name: data.community.name.clone(), + original_community_title: data.community.title.clone(), + original_community_banner: None, + original_community_description: None, + original_community_sidebar: None, + original_community_icon: None, + reason: "the ice cream incident".into(), + }; + let community_report = CommunityReport::report(pool, &community_report_form).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::Community(v) = &reports[0] { + assert!(!v.community_report.resolved); + assert_eq!(data.jessica.name, v.creator.name); + assert_eq!(community_report.reason, v.community_report.reason); + assert_eq!(data.community.name, v.community.name); + assert_eq!(data.community.title, v.community.title); + } else { + panic!("wrong type"); + } + + // admin resolves the report (after taking appropriate action) + CommunityReport::resolve(pool, community_report.id, data.admin_view.person.id).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::Community(v) = &reports[0] { + assert!(v.community_report.resolved); + assert!(v.resolver.is_some()); + assert_eq!( + Some(&data.admin_view.person.name), + v.resolver.as_ref().map(|r| &r.name) + ); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 7fe529eb6b..25aa473463 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -3,11 +3,18 @@ use diesel::Queryable; #[cfg(feature = "full")] use diesel::{deserialize::FromSqlRow, expression::AsExpression, sql_types}; use lemmy_db_schema::{ - aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates}, + aggregates::structs::{ + CommentAggregates, + CommunityAggregates, + PersonAggregates, + PostAggregates, + SiteAggregates, + }, source::{ comment::Comment, comment_report::CommentReport, community::Community, + community_report::CommunityReport, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, images::{ImageDetails, LocalImage}, @@ -80,6 +87,22 @@ pub struct CommentView { pub my_vote: Option, } +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A community report view. +pub struct CommunityReportView { + pub community_report: CommunityReport, + pub community: Community, + pub creator: Person, + pub counts: CommunityAggregates, + pub subscribed: SubscribedType, + #[cfg_attr(feature = "full", ts(optional))] + pub resolver: Option, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] @@ -271,9 +294,12 @@ pub struct ReportCombinedViewInternal { // Private-message-specific pub private_message_report: Option, pub private_message: Option, + // Community-specific + pub community_report: Option, + pub community_counts: Option, // Shared pub report_creator: Person, - pub item_creator: Person, + pub item_creator: Option, pub community: Option, pub subscribed: SubscribedType, pub resolver: Option, @@ -292,6 +318,7 @@ pub enum ReportCombinedView { Post(PostReportView), Comment(CommentReportView), PrivateMessage(PrivateMessageReportView), + Community(CommunityReportView), } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)] diff --git a/migrations/2024-12-27-220142_community_report/down.sql b/migrations/2024-12-27-220142_community_report/down.sql new file mode 100644 index 0000000000..1913ee3d22 --- /dev/null +++ b/migrations/2024-12-27-220142_community_report/down.sql @@ -0,0 +1,14 @@ +DELETE FROM report_combined +WHERE community_report_id IS NOT NULL; + +ALTER TABLE report_combined + DROP CONSTRAINT report_combined_check, + ADD CHECK (num_nonnulls (post_report_id, comment_report_id, private_message_report_id) = 1), + DROP COLUMN community_report_id; + +DROP TABLE community_report CASCADE; + +ALTER TABLE community_aggregates + DROP COLUMN report_count, + DROP COLUMN unresolved_report_count; + diff --git a/migrations/2024-12-27-220142_community_report/up.sql b/migrations/2024-12-27-220142_community_report/up.sql new file mode 100644 index 0000000000..fc39059920 --- /dev/null +++ b/migrations/2024-12-27-220142_community_report/up.sql @@ -0,0 +1,29 @@ +CREATE TABLE community_report ( + id serial PRIMARY KEY, + creator_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + original_community_name text NOT NULL, + original_community_title text NOT NULL, + original_community_description text, + original_community_sidebar text, + original_community_icon text, + original_community_banner text, + reason text NOT NULL, + resolved bool NOT NULL DEFAULT FALSE, + resolver_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE, + published timestamptz NOT NULL DEFAULT now(), + updated timestamptz NULL, + UNIQUE (community_id, creator_id) +); + +CREATE INDEX idx_community_report_published ON community_report (published DESC); + +ALTER TABLE report_combined + ADD COLUMN community_report_id int UNIQUE REFERENCES community_report ON UPDATE CASCADE ON DELETE CASCADE, + DROP CONSTRAINT report_combined_check, + ADD CHECK (num_nonnulls (post_report_id, comment_report_id, private_message_report_id, community_report_id) = 1); + +ALTER TABLE community_aggregates + ADD COLUMN report_count smallint NOT NULL DEFAULT 0, + ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0; +