lemmy/crates/db_schema/src/impls/community.rs
Nutomic 9a64902ace
Remove aggregate tables (fixes #4869) (#5407)
* migration

* update code

* tests

* triggers

* fix

* fmt

* clippy

* post aggregate migration

* changes for post aggregate code

* wip: update tests for post aggregate

* format

* fix partialeq

* trigger fix

* fix post insert trigger

* wip

* reorder

* fixes

* community aggregate migration

* update code

* triggers

* person aggregate migration

* person aggregate code

* person triggers

* test fixes

* fix scheduled task

* update api tests

* site_aggregates to local_site migration

* site_aggregates code changes

* triggers, tests

* more fixes

* Rename PersonPostAggregates to PostActions

* Merge local_user_vote_display_mode into local_user

* fix schema

* remove duplicate fields

* remove "aggregates" from index names

* uncomment indices

* if count = 0

* remove commentaggregates

* Fix triggers in remove aggregates tables pr (#5451)

* prevent all db_schema test errors

* fix the delete_comments_before_post problem in a way that doesn't affect the returned number of affected rows

* remove unnecessary recursion checks and add comment to remaining check

* clean up

* Fixing SQL format.

* Update triggers.sql

* Update triggers.sql

* Update triggers.sql

* Update triggers.sql

* remove update of deleted column

---------

Co-authored-by: Dessalines <tyhou13@gmx.com>

* rename migration

* Fix migration errors

* Fixing person_saved_combined. (#5481)

---------

Co-authored-by: dullbananas <dull.bananas0@gmail.com>
Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2025-03-06 16:18:41 +01:00

911 lines
29 KiB
Rust

use crate::{
diesel::{DecoratableTarget, OptionalExtension},
newtypes::{CommunityId, DbUrl, PersonId},
schema::{community, community_actions, instance, post},
source::{
actor_language::CommunityLanguage,
community::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
CommunityPersonBan,
CommunityPersonBanForm,
CommunityUpdateForm,
},
post::Post,
},
traits::{ApubActor, Bannable, Crud, Followable, Joinable},
utils::{
functions::{coalesce, coalesce_2_nullable, lower, random_smallint},
get_conn,
now,
uplete,
DbPool,
},
CommunityVisibility,
ListingType,
SubscribedType,
};
use chrono::{DateTime, Utc};
use diesel::{
deserialize,
dsl::{exists, insert_into, not},
expression::SelectableHelper,
pg::Pg,
result::Error,
select,
sql_types,
update,
BoolExpressionMethods,
ExpressionMethods,
NullableExpressionMethods,
QueryDsl,
Queryable,
};
use diesel_async::RunQueryDsl;
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
settings::structs::Settings,
};
use url::Url;
impl Crud for Community {
type InsertForm = CommunityInsertForm;
type UpdateForm = CommunityUpdateForm;
type IdType = CommunityId;
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let community_ = insert_into(community::table)
.values(form)
.get_result::<Self>(conn)
.await?;
// Initialize languages for new community
CommunityLanguage::update(pool, vec![], community_.id).await?;
Ok(community_)
}
async fn update(
pool: &mut DbPool<'_>,
community_id: CommunityId,
form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(community::table.find(community_id))
.set(form)
.get_result::<Self>(conn)
.await
}
}
impl Joinable for CommunityModerator {
type Form = CommunityModeratorForm;
async fn join(
pool: &mut DbPool<'_>,
community_moderator_form: &CommunityModeratorForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let community_moderator_form = (
community_moderator_form,
community_actions::became_moderator.eq(now().nullable()),
);
insert_into(community_actions::table)
.values(community_moderator_form)
.on_conflict((
community_actions::person_id,
community_actions::community_id,
))
.do_update()
.set(community_moderator_form)
.returning(Self::as_select())
.get_result::<Self>(conn)
.await
}
async fn leave(
pool: &mut DbPool<'_>,
community_moderator_form: &CommunityModeratorForm,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(community_actions::table.find((
community_moderator_form.person_id,
community_moderator_form.community_id,
)))
.set_null(community_actions::became_moderator)
.get_result(conn)
.await
}
}
pub enum CollectionType {
Moderators,
Featured,
}
impl Community {
pub async fn insert_apub(
pool: &mut DbPool<'_>,
timestamp: DateTime<Utc>,
form: &CommunityInsertForm,
) -> Result<Self, Error> {
let is_new_community = match &form.ap_id {
Some(id) => Community::read_from_apub_id(pool, id).await?.is_none(),
None => true,
};
let conn = &mut get_conn(pool).await?;
// Can't do separate insert/update commands because InsertForm/UpdateForm aren't convertible
let community_ = insert_into(community::table)
.values(form)
.on_conflict(community::ap_id)
.filter_target(coalesce(community::updated, community::published).lt(timestamp))
.do_update()
.set(form)
.get_result::<Self>(conn)
.await?;
// Initialize languages for new community
if is_new_community {
CommunityLanguage::update(pool, vec![], community_.id).await?;
}
Ok(community_)
}
/// Get the community which has a given moderators or featured url, also return the collection
/// type
pub async fn get_by_collection_url(
pool: &mut DbPool<'_>,
url: &DbUrl,
) -> LemmyResult<(Community, CollectionType)> {
let conn = &mut get_conn(pool).await?;
let res = community::table
.filter(community::moderators_url.eq(url))
.first(conn)
.await;
if let Ok(c) = res {
Ok((c, CollectionType::Moderators))
} else {
let res = community::table
.filter(community::featured_url.eq(url))
.first(conn)
.await;
if let Ok(c) = res {
Ok((c, CollectionType::Featured))
} else {
Err(LemmyErrorType::NotFound.into())
}
}
}
pub async fn set_featured_posts(
community_id: CommunityId,
posts: Vec<Post>,
pool: &mut DbPool<'_>,
) -> Result<(), Error> {
let conn = &mut get_conn(pool).await?;
for p in &posts {
debug_assert!(p.community_id == community_id);
}
// Mark the given posts as featured and all other posts as not featured.
let post_ids = posts.iter().map(|p| p.id);
update(post::table)
.filter(post::community_id.eq(community_id))
// This filter is just for performance
.filter(post::featured_community.or(post::id.eq_any(post_ids.clone())))
.set(post::featured_community.eq(post::id.eq_any(post_ids)))
.execute(conn)
.await?;
Ok(())
}
pub async fn get_random_community_id(
pool: &mut DbPool<'_>,
type_: &Option<ListingType>,
show_nsfw: Option<bool>,
) -> Result<CommunityId, Error> {
let conn = &mut get_conn(pool).await?;
// This is based on the random page selection algorithm in MediaWiki. It assigns a random number
// X to each item. To pick a random one, it generates a random number Y and gets the item with
// the lowest X value where X >= Y.
//
// https://phabricator.wikimedia.org/source/mediawiki/browse/master/includes/specials/SpecialRandomPage.php;763c5f084101676ab1bc52862e1ffbd24585a365
//
// The difference is we also regenerate the item's assigned number when the item is picked.
// Without this, items would have permanent variations in the probability of being picked.
// Additionally, in each group of multiple items that are assigned the same random number (a
// more likely occurence with `smallint`), there would be only one item that ever gets
// picked.
let try_pick = || {
let mut query = community::table
.filter(not(
community::deleted
.or(community::removed)
.or(community::visibility.eq(CommunityVisibility::Private)),
))
.order(community::random_number.asc())
.select(community::id)
.into_boxed();
if let Some(ListingType::Local) = type_ {
query = query.filter(community::local);
}
if !show_nsfw.unwrap_or(false) {
query = query.filter(not(community::nsfw));
}
query
};
diesel::update(community::table)
.filter(
community::id.nullable().eq(coalesce_2_nullable(
try_pick()
.filter(community::random_number.nullable().ge(
// Without `select` and `single_value`, this would call `random_smallint` separately
// for each row
select(random_smallint()).single_value(),
))
.single_value(),
// Wrap to the beginning if the generated number is higher than all
// `community::random_number` values, just like in the MediaWiki algorithm
try_pick().single_value(),
)),
)
.set(community::random_number.eq(random_smallint()))
.returning(community::id)
.get_result::<CommunityId>(conn)
.await
}
#[diesel::dsl::auto_type(no_type_alias)]
pub fn hide_removed_and_deleted() -> _ {
community::removed
.eq(false)
.and(community::deleted.eq(false))
}
pub fn local_url(name: &str, settings: &Settings) -> LemmyResult<DbUrl> {
let domain = settings.get_protocol_and_hostname();
Ok(Url::parse(&format!("{domain}/c/{name}"))?.into())
}
pub async fn update_federated_followers(
pool: &mut DbPool<'_>,
for_community_id: CommunityId,
new_subscribers: i32,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let new_subscribers: i64 = new_subscribers.into();
diesel::update(community::table.find(for_community_id))
.set(community::dsl::subscribers.eq(new_subscribers))
.get_result(conn)
.await
}
}
impl CommunityModerator {
pub async fn delete_for_community(
pool: &mut DbPool<'_>,
for_community_id: CommunityId,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(
community_actions::table.filter(community_actions::community_id.eq(for_community_id)),
)
.set_null(community_actions::became_moderator)
.get_result(conn)
.await
}
pub async fn leave_all_communities(
pool: &mut DbPool<'_>,
for_person_id: PersonId,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(community_actions::table.filter(community_actions::person_id.eq(for_person_id)))
.set_null(community_actions::became_moderator)
.get_result(conn)
.await
}
pub async fn get_person_moderated_communities(
pool: &mut DbPool<'_>,
for_person_id: PersonId,
) -> Result<Vec<CommunityId>, Error> {
let conn = &mut get_conn(pool).await?;
community_actions::table
.filter(community_actions::became_moderator.is_not_null())
.filter(community_actions::person_id.eq(for_person_id))
.select(community_actions::community_id)
.load::<CommunityId>(conn)
.await
}
/// Checks to make sure the acting moderator was added earlier than the target moderator
pub async fn is_higher_mod_check(
pool: &mut DbPool<'_>,
for_community_id: CommunityId,
mod_person_id: PersonId,
target_person_ids: Vec<PersonId>,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
// Build the list of persons
let mut persons = target_person_ids;
persons.push(mod_person_id);
persons.dedup();
let res = community_actions::table
.filter(community_actions::became_moderator.is_not_null())
.filter(community_actions::community_id.eq(for_community_id))
.filter(community_actions::person_id.eq_any(persons))
.order_by(community_actions::became_moderator)
.select(community_actions::person_id)
// This does a limit 1 select first
.first::<PersonId>(conn)
.await?;
// If the first result sorted by published is the acting mod
if res == mod_person_id {
Ok(())
} else {
Err(LemmyErrorType::NotHigherMod)?
}
}
}
impl Bannable for CommunityPersonBan {
type Form = CommunityPersonBanForm;
async fn ban(
pool: &mut DbPool<'_>,
community_person_ban_form: &CommunityPersonBanForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let community_person_ban_form = (
community_person_ban_form,
community_actions::received_ban.eq(now().nullable()),
);
insert_into(community_actions::table)
.values(community_person_ban_form)
.on_conflict((
community_actions::community_id,
community_actions::person_id,
))
.do_update()
.set(community_person_ban_form)
.returning(Self::as_select())
.get_result::<Self>(conn)
.await
}
async fn unban(
pool: &mut DbPool<'_>,
community_person_ban_form: &CommunityPersonBanForm,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(community_actions::table.find((
community_person_ban_form.person_id,
community_person_ban_form.community_id,
)))
.set_null(community_actions::received_ban)
.set_null(community_actions::ban_expires)
.get_result(conn)
.await
}
}
impl CommunityFollower {
/// Check if a remote instance has any followers on local instance. For this it is enough to check
/// if any follow relation is stored. Dont use this for local community.
pub async fn check_has_local_followers(
pool: &mut DbPool<'_>,
remote_community_id: CommunityId,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
let find_action = community_actions::table
.filter(community_actions::followed.is_not_null())
.filter(community_actions::community_id.eq(remote_community_id));
select(exists(find_action))
.get_result::<bool>(conn)
.await?
.then_some(())
.ok_or(LemmyErrorType::CommunityHasNoFollowers.into())
}
pub async fn approve(
pool: &mut DbPool<'_>,
community_id: CommunityId,
follower_id: PersonId,
approver_id: PersonId,
) -> LemmyResult<()> {
let conn = &mut get_conn(pool).await?;
let find_action = community_actions::table
.find((follower_id, community_id))
.filter(community_actions::followed.is_not_null());
diesel::update(find_action)
.set((
community_actions::follow_state.eq(CommunityFollowerState::Accepted),
community_actions::follow_approver_id.eq(approver_id),
))
.execute(conn)
.await?;
Ok(())
}
}
// TODO
// I'd really like to have these on the impl, but unfortunately they have to be top level,
// according to https://diesel.rs/guides/composing-applications.html
#[diesel::dsl::auto_type]
pub fn community_follower_select_subscribed_type() -> _ {
community_actions::follow_state.nullable()
}
impl Queryable<sql_types::Nullable<crate::schema::sql_types::CommunityFollowerState>, Pg>
for SubscribedType
{
type Row = Option<CommunityFollowerState>;
fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(match row {
Some(CommunityFollowerState::Pending) => SubscribedType::Pending,
Some(CommunityFollowerState::Accepted) => SubscribedType::Subscribed,
Some(CommunityFollowerState::ApprovalRequired) => SubscribedType::ApprovalRequired,
None => SubscribedType::NotSubscribed,
})
}
}
impl Followable for CommunityFollower {
type Form = CommunityFollowerForm;
async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let form = (form, community_actions::followed.eq(now().nullable()));
insert_into(community_actions::table)
.values(form)
.on_conflict((
community_actions::community_id,
community_actions::person_id,
))
.do_update()
.set(form)
.returning(Self::as_select())
.get_result::<Self>(conn)
.await
}
async fn follow_accepted(
pool: &mut DbPool<'_>,
community_id: CommunityId,
person_id: PersonId,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let find_action = community_actions::table
.find((person_id, community_id))
.filter(community_actions::follow_state.is_not_null());
diesel::update(find_action)
.set(community_actions::follow_state.eq(Some(CommunityFollowerState::Accepted)))
.returning(Self::as_select())
.get_result::<Self>(conn)
.await
}
async fn unfollow(
pool: &mut DbPool<'_>,
form: &CommunityFollowerForm,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(community_actions::table.find((form.person_id, form.community_id)))
.set_null(community_actions::followed)
.set_null(community_actions::follow_state)
.set_null(community_actions::follow_approver_id)
.get_result(conn)
.await
}
}
impl ApubActor for Community {
async fn read_from_apub_id(
pool: &mut DbPool<'_>,
object_id: &DbUrl,
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
community::table
.filter(community::ap_id.eq(object_id))
.first(conn)
.await
.optional()
}
async fn read_from_name(
pool: &mut DbPool<'_>,
community_name: &str,
include_deleted: bool,
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
let mut q = community::table
.into_boxed()
.filter(community::local.eq(true))
.filter(lower(community::name).eq(community_name.to_lowercase()));
if !include_deleted {
q = q.filter(Self::hide_removed_and_deleted())
}
q.first(conn).await.optional()
}
async fn read_from_name_and_domain(
pool: &mut DbPool<'_>,
community_name: &str,
for_domain: &str,
) -> Result<Option<Self>, Error> {
let conn = &mut get_conn(pool).await?;
community::table
.inner_join(instance::table)
.filter(lower(community::name).eq(community_name.to_lowercase()))
.filter(lower(instance::domain).eq(for_domain.to_lowercase()))
.select(community::all_columns)
.first(conn)
.await
.optional()
}
}
#[cfg(test)]
mod tests {
use crate::{
source::{
comment::{Comment, CommentInsertForm},
community::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityFollowerState,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
CommunityPersonBan,
CommunityPersonBanForm,
CommunityUpdateForm,
},
instance::Instance,
local_user::LocalUser,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
},
traits::{Bannable, Crud, Followable, Joinable},
utils::{build_db_pool_for_tests, uplete, RANK_DEFAULT},
CommunityVisibility,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let bobby_person = PersonInsertForm::test_form(inserted_instance.id, "bobby");
let inserted_bobby = Person::create(pool, &bobby_person).await?;
let artemis_person = PersonInsertForm::test_form(inserted_instance.id, "artemis");
let inserted_artemis = Person::create(pool, &artemis_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
"TIL".into(),
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await?;
let expected_community = Community {
id: inserted_community.id,
name: "TIL".into(),
title: "nada".to_owned(),
sidebar: None,
description: None,
nsfw: false,
removed: false,
deleted: false,
published: inserted_community.published,
updated: None,
ap_id: inserted_community.ap_id.clone(),
local: true,
private_key: None,
public_key: "pubkey".to_owned(),
last_refreshed_at: inserted_community.published,
icon: None,
banner: None,
followers_url: inserted_community.followers_url.clone(),
inbox_url: inserted_community.inbox_url.clone(),
moderators_url: None,
featured_url: None,
hidden: false,
posting_restricted_to_mods: false,
instance_id: inserted_instance.id,
visibility: CommunityVisibility::Public,
random_number: inserted_community.random_number,
subscribers: 1,
posts: 0,
comments: 0,
users_active_day: 0,
users_active_week: 0,
users_active_month: 0,
users_active_half_year: 0,
hot_rank: RANK_DEFAULT,
subscribers_local: 1,
report_count: 0,
unresolved_report_count: 0,
interactions_month: 0,
};
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
let inserted_community_follower =
CommunityFollower::follow(pool, &community_follower_form).await?;
let expected_community_follower = CommunityFollower {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
state: CommunityFollowerState::Accepted,
published: inserted_community_follower.published,
approver_id: None,
};
let bobby_moderator_form = CommunityModeratorForm {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
};
let inserted_bobby_moderator = CommunityModerator::join(pool, &bobby_moderator_form).await?;
let artemis_moderator_form = CommunityModeratorForm {
community_id: inserted_community.id,
person_id: inserted_artemis.id,
};
let _inserted_artemis_moderator =
CommunityModerator::join(pool, &artemis_moderator_form).await?;
let expected_community_moderator = CommunityModerator {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
published: inserted_bobby_moderator.published,
};
let moderator_person_ids = vec![inserted_bobby.id, inserted_artemis.id];
// Make sure bobby is marked as a higher mod than artemis, and vice versa
let bobby_higher_check = CommunityModerator::is_higher_mod_check(
pool,
inserted_community.id,
inserted_bobby.id,
moderator_person_ids.clone(),
)
.await;
assert!(bobby_higher_check.is_ok());
// Also check the other is_higher_mod_or_admin function just in case
let bobby_higher_check_2 = LocalUser::is_higher_mod_or_admin_check(
pool,
inserted_community.id,
inserted_bobby.id,
moderator_person_ids.clone(),
)
.await;
assert!(bobby_higher_check_2.is_ok());
// This should throw an error, since artemis was added later
let artemis_higher_check = CommunityModerator::is_higher_mod_check(
pool,
inserted_community.id,
inserted_artemis.id,
moderator_person_ids,
)
.await;
assert!(artemis_higher_check.is_err());
let community_person_ban_form = CommunityPersonBanForm {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
expires: None,
};
let inserted_community_person_ban =
CommunityPersonBan::ban(pool, &community_person_ban_form).await?;
let expected_community_person_ban = CommunityPersonBan {
community_id: inserted_community.id,
person_id: inserted_bobby.id,
published: inserted_community_person_ban.published,
expires: None,
};
let read_community = Community::read(pool, inserted_community.id).await?;
let update_community_form = CommunityUpdateForm {
title: Some("nada".to_owned()),
..Default::default()
};
let updated_community =
Community::update(pool, inserted_community.id, &update_community_form).await?;
let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form).await?;
let left_community = CommunityModerator::leave(pool, &bobby_moderator_form).await?;
let unban = CommunityPersonBan::unban(pool, &community_person_ban_form).await?;
let num_deleted = Community::delete(pool, inserted_community.id).await?;
Person::delete(pool, inserted_bobby.id).await?;
Person::delete(pool, inserted_artemis.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
assert_eq!(expected_community, read_community);
assert_eq!(expected_community, updated_community);
assert_eq!(expected_community_follower, inserted_community_follower);
assert_eq!(expected_community_moderator, inserted_bobby_moderator);
assert_eq!(expected_community_person_ban, inserted_community_person_ban);
assert_eq!(uplete::Count::only_updated(1), ignored_community);
assert_eq!(uplete::Count::only_updated(1), left_community);
assert_eq!(uplete::Count::only_deleted(1), unban);
// assert_eq!(2, loaded_count);
assert_eq!(1, num_deleted);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_aggregates() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg");
let inserted_person = Person::create(pool, &new_person).await?;
let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg");
let another_inserted_person = Person::create(pool, &another_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
"TIL_community_agg".into(),
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await?;
let another_community = CommunityInsertForm::new(
inserted_instance.id,
"TIL_community_agg_2".into(),
"nada".to_owned(),
"pubkey".to_string(),
);
let another_inserted_community = Community::create(pool, &another_community).await?;
let first_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: inserted_person.id,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
CommunityFollower::follow(pool, &first_person_follow).await?;
let second_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
person_id: another_inserted_person.id,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
CommunityFollower::follow(pool, &second_person_follow).await?;
let another_community_follow = CommunityFollowerForm {
community_id: another_inserted_community.id,
person_id: inserted_person.id,
state: Some(CommunityFollowerState::Accepted),
approver_id: None,
};
CommunityFollower::follow(pool, &another_community_follow).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let child_comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let _inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let community_aggregates_before_delete = Community::read(pool, inserted_community.id).await?;
assert_eq!(2, community_aggregates_before_delete.subscribers);
assert_eq!(2, community_aggregates_before_delete.subscribers_local);
assert_eq!(1, community_aggregates_before_delete.posts);
assert_eq!(2, community_aggregates_before_delete.comments);
// Test the other community
let another_community_aggs = Community::read(pool, another_inserted_community.id).await?;
assert_eq!(1, another_community_aggs.subscribers);
assert_eq!(1, another_community_aggs.subscribers_local);
assert_eq!(0, another_community_aggs.posts);
assert_eq!(0, another_community_aggs.comments);
// Unfollow test
CommunityFollower::unfollow(pool, &second_person_follow).await?;
let after_unfollow = Community::read(pool, inserted_community.id).await?;
assert_eq!(1, after_unfollow.subscribers);
assert_eq!(1, after_unfollow.subscribers_local);
// Follow again just for the later tests
CommunityFollower::follow(pool, &second_person_follow).await?;
let after_follow_again = Community::read(pool, inserted_community.id).await?;
assert_eq!(2, after_follow_again.subscribers);
assert_eq!(2, after_follow_again.subscribers_local);
// Remove a parent post (the comment count should also be 0)
Post::delete(pool, inserted_post.id).await?;
let after_parent_post_delete = Community::read(pool, inserted_community.id).await?;
assert_eq!(0, after_parent_post_delete.posts);
assert_eq!(0, after_parent_post_delete.comments);
// Remove the 2nd person
Person::delete(pool, another_inserted_person.id).await?;
let after_person_delete = Community::read(pool, inserted_community.id).await?;
assert_eq!(1, after_person_delete.subscribers);
assert_eq!(1, after_person_delete.subscribers_local);
// This should delete all the associated rows, and fire triggers
let person_num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, person_num_deleted);
// Delete the community
let community_num_deleted = Community::delete(pool, inserted_community.id).await?;
assert_eq!(1, community_num_deleted);
let another_community_num_deleted =
Community::delete(pool, another_inserted_community.id).await?;
assert_eq!(1, another_community_num_deleted);
// Should be none found, since the creator was deleted
let after_delete = Community::read(pool, inserted_community.id).await;
assert!(after_delete.is_err());
Ok(())
}
}