Add community reports (only the database part) (#4996)

* database stuff, not including tests

* change migration date

* fix community_report_view

* update stuff related to report_combined

* add db_schema/src/impls/community_report.rs

* add report counts to community_aggregates

* fix community_report columns and update report_combined_view::tests::test_combined

* add column for original sidebar; use None instead of clone; add report_combined_view::tests::test_community_reports

* use ts(optional) in CommunityReportView

* remove CommunityReportView::read
This commit is contained in:
dullbananas 2025-01-13 03:14:56 -07:00 committed by GitHub
parent 11e0513592
commit 4d17eef82b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 443 additions and 20 deletions

View file

@ -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,6 +704,8 @@ 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');
-- person_content (comment, post)
CREATE PROCEDURE r.create_person_content_combined_trigger (table_name text)
LANGUAGE plpgsql

View file

@ -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)]

View file

@ -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<Self, Error> {
let conn = &mut get_conn(pool).await?;
insert_into(community_report)
.values(community_report_form)
.get_result::<Self>(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<usize, Error> {
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<usize, Error> {
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<usize, Error> {
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
}
}

View file

@ -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;

View file

@ -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))]

View file

@ -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<Text>,
original_community_sidebar -> Nullable<Text>,
original_community_icon -> Nullable<Text>,
original_community_banner -> Nullable<Text>,
reason -> Text,
resolved -> Bool,
resolver_id -> Nullable<Int4>,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
}
}
diesel::table! {
custom_emoji (id) {
id -> Int4,
@ -922,6 +943,7 @@ diesel::table! {
post_report_id -> Nullable<Int4>,
comment_report_id -> Nullable<Int4>,
private_message_report_id -> Nullable<Int4>,
community_report_id -> Nullable<Int4>,
}
}
@ -1040,6 +1062,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));
@ -1099,6 +1122,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));
@ -1124,6 +1148,7 @@ diesel::allow_tables_to_appear_in_same_query!(
community_actions,
community_aggregates,
community_language,
community_report,
custom_emoji,
custom_emoji_keyword,
email_verification,

View file

@ -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<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub original_community_sidebar: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub original_community_icon: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub original_community_banner: Option<String>,
pub reason: String,
pub resolved: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub resolver_id: Option<PersonId>,
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>,
}
#[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<String>,
pub original_community_sidebar: Option<String>,
pub original_community_icon: Option<DbUrl>,
pub original_community_banner: Option<DbUrl>,
pub reason: String,
}

View file

@ -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;

View file

@ -1,6 +1,7 @@
use crate::{
structs::{
CommentReportView,
CommunityReportView,
LocalUserView,
PostReportView,
PrivateMessageReportView,
@ -32,6 +33,8 @@ use lemmy_db_schema::{
comment_report,
community,
community_actions,
community_aggregates,
community_report,
local_user,
person,
person_actions,
@ -67,6 +70,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
@ -87,6 +91,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();
@ -114,6 +119,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}"))
@ -130,6 +136,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?;
@ -171,13 +178,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(report_creator)
.or(comment_report::creator_id.eq(report_creator))
.or(private_message_report::creator_id.eq(report_creator)),
.or(private_message_report::creator_id.eq(report_creator))
.or(community_report::creator_id.eq(report_creator)),
),
)
// The comment
@ -196,7 +205,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)
@ -205,7 +214,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,
@ -221,7 +236,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(
@ -233,13 +248,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(
@ -270,9 +290,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()),
@ -290,12 +313,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);
@ -316,6 +347,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,
@ -344,12 +376,20 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
// Use for a short alias
let v = self.clone();
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,
@ -358,7 +398,7 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
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,
@ -370,12 +410,20 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
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,
v.community,
v.community.clone(),
v.item_creator.clone(),
) {
Some(ReportCombinedView::Comment(CommentReportView {
comment_report,
@ -384,7 +432,7 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
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,
@ -394,18 +442,32 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
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
}
@ -433,6 +495,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,
@ -558,6 +621,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,
@ -599,9 +676,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);
@ -624,7 +706,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
@ -971,4 +1053,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(())
}
}

View file

@ -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<i16>,
}
#[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<Person>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
@ -283,9 +306,12 @@ pub struct ReportCombinedViewInternal {
// Private-message-specific
pub private_message_report: Option<PrivateMessageReport>,
pub private_message: Option<PrivateMessage>,
// Community-specific
pub community_report: Option<CommunityReport>,
pub community_counts: Option<CommunityAggregates>,
// Shared
pub report_creator: Person,
pub item_creator: Person,
pub item_creator: Option<Person>,
pub community: Option<Community>,
pub subscribed: SubscribedType,
pub resolver: Option<Person>,
@ -304,6 +330,7 @@ pub enum ReportCombinedView {
Post(PostReportView),
Comment(CommentReportView),
PrivateMessage(PrivateMessageReportView),
Community(CommunityReportView),
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]

View file

@ -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;

View file

@ -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;