Finishing up combined person_saved and person_content.

This commit is contained in:
Dessalines 2024-12-07 15:46:46 -05:00
parent 32b5411abd
commit 3abc46fad9
14 changed files with 486 additions and 567 deletions

View file

@ -16,10 +16,7 @@ pub async fn save_comment(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> {
let comment_saved_form = CommentSavedForm {
comment_id: data.comment_id,
person_id: local_user_view.person.id,
};
let comment_saved_form = CommentSavedForm::new(data.comment_id, local_user_view.person.id);
if data.save {
CommentSaved::save(&mut context.pool(), &comment_saved_form)

View file

@ -0,0 +1,40 @@
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_common::{
context::LemmyContext,
person::{ListPersonSaved, ListPersonSavedResponse},
utils::check_private_instance,
};
use lemmy_db_views::{
person_saved_combined_view::PersonSavedCombinedQuery,
structs::{LocalUserView, SiteView},
};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn list_person_saved(
data: Query<ListPersonSaved>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPersonSavedResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?;
check_private_instance(&Some(local_user_view.clone()), &local_site.local_site)?;
// parse pagination token
let page_after = if let Some(pa) = &data.page_cursor {
Some(pa.read(&mut context.pool()).await?)
} else {
None
};
let page_back = data.page_back;
let saved = PersonSavedCombinedQuery {
page_after,
page_back,
}
.list(&mut context.pool(), &local_user_view)
.await?;
Ok(Json(ListPersonSavedResponse { saved }))
}

View file

@ -11,6 +11,7 @@ use lemmy_db_views::structs::{
LocalImageView,
PersonContentCombinedPaginationCursor,
PersonContentCombinedView,
PersonSavedCombinedPaginationCursor,
};
use lemmy_db_views_actor::structs::{
CommentReplyView,
@ -273,9 +274,9 @@ pub struct ListPersonContentResponse {
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Gets your saved posts and comments
pub struct ListSaved {
pub struct ListPersonSaved {
#[cfg_attr(feature = "full", ts(optional))]
pub page_cursor: Option<PersonContentCombinedPaginationCursor>,
pub page_cursor: Option<PersonSavedCombinedPaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_back: Option<bool>,
}
@ -285,7 +286,7 @@ pub struct ListSaved {
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A person's saved content response.
pub struct ListSavedResponse {
pub struct ListPersonSavedResponse {
pub saved: Vec<PersonContentCombinedView>,
}

View file

@ -212,10 +212,7 @@ pub async fn import_settings(
&context,
|(saved, context)| async move {
let comment = saved.dereference(&context).await?;
let form = CommentSavedForm {
person_id,
comment_id: comment.id,
};
let form = CommentSavedForm::new(comment.id, person_id);
CommentSaved::save(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
},

View file

@ -714,33 +714,74 @@ CALL r.create_person_content_combined_trigger ('post');
CALL r.create_person_content_combined_trigger ('comment');
-- person_saved (comment, post)
-- TODO, not sure how to handle changes to post_actions and comment_actions.saved column.
-- False should delete this row, true should insert
-- CREATE PROCEDURE r.create_person_saved_combined_trigger (table_name text)
-- LANGUAGE plpgsql
-- AS $a$
-- BEGIN
-- EXECUTE replace($b$ CREATE FUNCTION r.person_saved_combined_thing_insert ( )
-- RETURNS TRIGGER
-- LANGUAGE plpgsql
-- AS $$
-- BEGIN
-- INSERT INTO person_saved_combined (published, thing_id)
-- VALUES (NEW.saved, NEW.id);
-- RETURN NEW;
-- END $$;
-- CREATE TRIGGER person_saved_combined
-- AFTER INSERT ON thing
-- FOR EACH ROW
-- EXECUTE FUNCTION r.person_saved_combined_thing_insert ( );
-- $b$,
-- 'thing',
-- table_name);
-- END;
-- $a$;
-- TODO, not sure how to handle changes to post_actions and comment_actions.saved column using @dullbanana's trigger method.
-- Post
CREATE FUNCTION r.person_saved_combined_change_values_post ()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
DELETE FROM person_saved_combined AS p
WHERE p.person_id = OLD.person_id
AND p.post_id = OLD.post_id;
ELSIF (TG_OP = 'INSERT') THEN
IF NEW.saved IS NOT NULL THEN
INSERT INTO person_saved_combined (published, person_id, post_id)
VALUES (NEW.saved, NEW.person_id, NEW.post_id);
END IF;
ELSIF (TG_OP = 'UPDATE') THEN
IF NEW.saved IS NOT NULL THEN
INSERT INTO person_saved_combined (published, person_id, post_id)
VALUES (NEW.saved, NEW.person_id, NEW.post_id);
-- If saved gets set as null, delete the row
ELSE
DELETE FROM person_saved_combined AS p
WHERE p.person_id = NEW.person_id
AND p.post_id = NEW.post_id;
END IF;
END IF;
RETURN NULL;
END
$$;
-- CALL r.create_person_saved_combined_trigger ('post_actions');
CREATE TRIGGER person_saved_combined_post
AFTER INSERT OR DELETE OR UPDATE OF saved ON post_actions
FOR EACH ROW
EXECUTE FUNCTION r.person_saved_combined_change_values_post ();
-- CALL r.create_person_saved_combined_trigger ('comment_actions');
-- Comment
CREATE FUNCTION r.person_saved_combined_change_values_comment ()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
DELETE FROM person_saved_combined AS p
WHERE p.person_id = OLD.person_id
AND p.comment_id = OLD.comment_id;
ELSIF (TG_OP = 'INSERT') THEN
IF NEW.saved IS NOT NULL THEN
INSERT INTO person_saved_combined (published, person_id, comment_id)
VALUES (NEW.saved, NEW.person_id, NEW.comment_id);
END IF;
ELSIF (TG_OP = 'UPDATE') THEN
IF NEW.saved IS NOT NULL THEN
INSERT INTO person_saved_combined (published, person_id, comment_id)
VALUES (NEW.saved, NEW.person_id, NEW.comment_id);
-- If saved gets set as null, delete the row
ELSE
DELETE FROM person_saved_combined AS p
WHERE p.person_id = NEW.person_id
AND p.comment_id = NEW.comment_id;
END IF;
END IF;
RETURN NULL;
END
$$;
CREATE TRIGGER person_saved_combined_comment
AFTER INSERT OR DELETE OR UPDATE OF saved ON comment_actions
FOR EACH ROW
EXECUTE FUNCTION r.person_saved_combined_change_values_comment ();

View file

@ -184,10 +184,6 @@ impl Saveable for CommentSaved {
comment_saved_form: &CommentSavedForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let comment_saved_form = (
comment_saved_form,
comment_actions::saved.eq(now().nullable()),
);
insert_into(comment_actions::table)
.values(comment_saved_form)
.on_conflict((comment_actions::comment_id, comment_actions::person_id))
@ -319,11 +315,7 @@ mod tests {
};
// Comment Saved
let comment_saved_form = CommentSavedForm {
comment_id: inserted_comment.id,
person_id: inserted_person.id,
};
let comment_saved_form = CommentSavedForm::new(inserted_comment.id, inserted_person.id);
let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await?;
let expected_comment_saved = CommentSaved {

View file

@ -752,6 +752,7 @@ diesel::table! {
person_saved_combined (id) {
id -> Int4,
published -> Timestamptz,
person_id -> Int4,
post_id -> Nullable<Int4>,
comment_id -> Nullable<Int4>,
}
@ -1053,6 +1054,7 @@ diesel::joinable!(person_content_combined -> post (post_id));
diesel::joinable!(person_mention -> comment (comment_id));
diesel::joinable!(person_mention -> person (recipient_id));
diesel::joinable!(person_saved_combined -> comment (comment_id));
diesel::joinable!(person_saved_combined -> person (person_id));
diesel::joinable!(person_saved_combined -> post (post_id));
diesel::joinable!(post -> community (community_id));
diesel::joinable!(post -> language (language_id));

View file

@ -1,4 +1,4 @@
use crate::newtypes::{CommentId, PersonSavedCombinedId, PostId};
use crate::newtypes::{CommentId, PersonId, PersonSavedCombinedId, PostId};
#[cfg(feature = "full")]
use crate::schema::person_saved_combined;
use chrono::{DateTime, Utc};
@ -23,6 +23,7 @@ use ts_rs::TS;
pub struct PersonSavedCombined {
pub id: PersonSavedCombinedId,
pub published: DateTime<Utc>,
pub person_id: PersonId,
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>,
#[cfg_attr(feature = "full", ts(optional))]

View file

@ -142,7 +142,10 @@ pub struct CommentSaved {
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = comment_actions))]
#[derive(derive_new::new)]
pub struct CommentSavedForm {
pub comment_id: CommentId,
pub person_id: PersonId,
#[new(value = "Utc::now()")]
pub saved: DateTime<Utc>,
}

View file

@ -121,8 +121,8 @@ impl PersonContentCombinedQuery {
person::table.on(
comment::creator_id
.eq(item_creator)
// Need to filter out the post rows where both the post and comment creator are the
// same.
// Need to filter out the post rows where the post_id given is null
// Otherwise you'll get duped post rows
.or(
post::creator_id
.eq(item_creator)
@ -372,7 +372,7 @@ mod tests {
.await?;
assert_eq!(3, timmy_content.len());
// Make sure the report types are correct
// Make sure the types are correct
if let PersonContentCombinedView::Comment(v) = &timmy_content[0] {
assert_eq!(data.timmy_comment.id, v.comment.id);
assert_eq!(data.timmy.id, v.creator.id);

View file

@ -1,554 +1,393 @@
// use crate::{
// structs::{
// CommentView,
// LocalUserView,
// PostView,
// ProfileCombinedPaginationCursor,
// PersonContentCombinedView,
// PersonContentViewInternal,
// },
// InternalToCombinedView,
// };
// use diesel::{
// result::Error,
// BoolExpressionMethods,
// ExpressionMethods,
// JoinOnDsl,
// NullableExpressionMethods,
// QueryDsl,
// SelectableHelper,
// };
// use diesel_async::RunQueryDsl;
// use i_love_jesus::PaginatedQueryBuilder;
// use lemmy_db_schema::{
// aliases::creator_community_actions,
// newtypes::{CommunityId, PersonId},
// schema::{
// comment,
// comment_actions,
// comment_aggregates,
// community,
// community_actions,
// image_details,
// local_user,
// person,
// person_actions,
// post,
// post_actions,
// post_aggregates,
// profile_combined,
// },
// source::{
// combined::profile::{profile_combined_keys as key, ProfileCombined},
// community::CommunityFollower,
// },
// utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey},
// };
// use lemmy_utils::error::LemmyResult;
use crate::{
structs::{
LocalUserView,
PersonContentCombinedView,
PersonContentViewInternal,
PersonSavedCombinedPaginationCursor,
},
InternalToCombinedView,
};
use diesel::{
result::Error,
BoolExpressionMethods,
ExpressionMethods,
JoinOnDsl,
NullableExpressionMethods,
QueryDsl,
SelectableHelper,
};
use diesel_async::RunQueryDsl;
use i_love_jesus::PaginatedQueryBuilder;
use lemmy_db_schema::{
aliases::creator_community_actions,
schema::{
comment,
comment_actions,
comment_aggregates,
community,
community_actions,
image_details,
local_user,
person,
person_actions,
person_saved_combined,
post,
post_actions,
post_aggregates,
},
source::{
combined::person_saved::{person_saved_combined_keys as key, PersonSavedCombined},
community::CommunityFollower,
},
utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool},
};
use lemmy_utils::error::LemmyResult;
// impl ProfileCombinedPaginationCursor {
// // get cursor for page that starts immediately after the given post
// pub fn after_post(view: &PersonContentCombinedView) -> ProfileCombinedPaginationCursor {
// let (prefix, id) = match view {
// PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0),
// PersonContentCombinedView::Post(v) => ('P', v.post.id.0),
// };
// // hex encoding to prevent ossification
// ProfileCombinedPaginationCursor(format!("{prefix}{id:x}"))
// }
impl PersonSavedCombinedPaginationCursor {
// get cursor for page that starts immediately after the given post
pub fn after_post(view: &PersonContentCombinedView) -> PersonSavedCombinedPaginationCursor {
let (prefix, id) = match view {
PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0),
PersonContentCombinedView::Post(v) => ('P', v.post.id.0),
};
// hex encoding to prevent ossification
PersonSavedCombinedPaginationCursor(format!("{prefix}{id:x}"))
}
// pub async fn read(&self, pool: &mut DbPool<'_>) -> Result<PaginationCursorData, Error> {
// let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into());
// let mut query = profile_combined::table
// .select(ProfileCombined::as_select())
// .into_boxed();
// let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?;
// let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?;
// query = match prefix {
// "C" => query.filter(profile_combined::comment_id.eq(id)),
// "P" => query.filter(profile_combined::post_id.eq(id)),
// _ => return Err(err_msg()),
// };
// let token = query.first(&mut get_conn(pool).await?).await?;
pub async fn read(&self, pool: &mut DbPool<'_>) -> Result<PaginationCursorData, Error> {
let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into());
let mut query = person_saved_combined::table
.select(PersonSavedCombined::as_select())
.into_boxed();
let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?;
let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?;
query = match prefix {
"C" => query.filter(person_saved_combined::comment_id.eq(id)),
"P" => query.filter(person_saved_combined::post_id.eq(id)),
_ => return Err(err_msg()),
};
let token = query.first(&mut get_conn(pool).await?).await?;
// Ok(PaginationCursorData(token))
// }
// }
Ok(PaginationCursorData(token))
}
}
// #[derive(Clone)]
// pub struct PaginationCursorData(ProfileCombined);
#[derive(Clone)]
pub struct PaginationCursorData(PersonSavedCombined);
// #[derive(Default)]
// pub struct ProfileCombinedQuery {
// pub creator_id: PersonId,
// pub page_after: Option<PaginationCursorData>,
// pub page_back: Option<bool>,
// }
#[derive(Default)]
pub struct PersonSavedCombinedQuery {
pub page_after: Option<PaginationCursorData>,
pub page_back: Option<bool>,
}
// impl ProfileCombinedQuery {
// pub async fn list(
// self,
// pool: &mut DbPool<'_>,
// user: &Option<LocalUserView>,
// ) -> LemmyResult<Vec<PersonContentCombinedView>> {
// let my_person_id = user
// .as_ref()
// .map(|u| u.local_user.person_id)
// .unwrap_or(PersonId(-1));
// let item_creator = person::id;
impl PersonSavedCombinedQuery {
pub async fn list(
self,
pool: &mut DbPool<'_>,
user: &LocalUserView,
) -> LemmyResult<Vec<PersonContentCombinedView>> {
let my_person_id = user.local_user.person_id;
let item_creator = person::id;
// let conn = &mut get_conn(pool).await?;
let conn = &mut get_conn(pool).await?;
// // Notes: since the post_id and comment_id are optional columns,
// // many joins must use an OR condition.
// // For example, the creator must be the person table joined to either:
// // - post.creator_id
// // - comment.creator_id
// let mut query = profile_combined::table
// // The comment
// .left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable())))
// // The post
// .inner_join(
// post::table.on(
// profile_combined::post_id
// .eq(post::id.nullable())
// .or(comment::post_id.nullable().eq(profile_combined::post_id)),
// ),
// )
// // The item creator
// .inner_join(
// person::table.on(
// comment::creator_id
// .eq(person::id)
// .or(post::creator_id.eq(person::id)),
// ),
// )
// // The community
// .inner_join(community::table.on(post::community_id.eq(community::id)))
// .left_join(actions_alias(
// creator_community_actions,
// item_creator,
// post::community_id,
// ))
// .left_join(
// local_user::table.on(
// item_creator
// .eq(local_user::person_id)
// .and(local_user::admin.eq(true)),
// ),
// )
// .left_join(actions(
// community_actions::table,
// Some(my_person_id),
// post::community_id,
// ))
// .left_join(actions(post_actions::table, Some(my_person_id), post::id))
// .left_join(actions(
// person_actions::table,
// Some(my_person_id),
// item_creator,
// ))
// .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id)))
// .left_join(
// comment_aggregates::table
// .on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())),
// )
// .left_join(actions(
// comment_actions::table,
// Some(my_person_id),
// comment::id,
// ))
// .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())))
// // The creator id filter
// .filter(item_creator.eq(self.creator_id))
// .select((
// // Post-specific
// post_aggregates::all_columns,
// coalesce(
// post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(),
// post_aggregates::comments,
// ),
// post_actions::saved.nullable().is_not_null(),
// post_actions::read.nullable().is_not_null(),
// post_actions::hidden.nullable().is_not_null(),
// post_actions::like_score.nullable(),
// image_details::all_columns.nullable(),
// // Comment-specific
// comment::all_columns.nullable(),
// comment_aggregates::all_columns.nullable(),
// comment_actions::saved.nullable().is_not_null(),
// comment_actions::like_score.nullable(),
// // Shared
// post::all_columns,
// community::all_columns,
// person::all_columns,
// CommunityFollower::select_subscribed_type(),
// local_user::admin.nullable().is_not_null(),
// creator_community_actions
// .field(community_actions::became_moderator)
// .nullable()
// .is_not_null(),
// creator_community_actions
// .field(community_actions::received_ban)
// .nullable()
// .is_not_null(),
// person_actions::blocked.nullable().is_not_null(),
// community_actions::received_ban.nullable().is_not_null(),
// ))
// .into_boxed();
// Notes: since the post_id and comment_id are optional columns,
// many joins must use an OR condition.
// For example, the creator must be the person table joined to either:
// - post.creator_id
// - comment.creator_id
let query = person_saved_combined::table
// The comment
.left_join(comment::table.on(person_saved_combined::comment_id.eq(comment::id.nullable())))
// The post
// It gets a bit complicated here, because since both comments and post combined have a post
// attached, you can do an inner join.
.inner_join(
post::table.on(
person_saved_combined::post_id
.eq(post::id.nullable())
.or(comment::post_id.eq(post::id)),
),
)
// The item creator
.inner_join(
person::table.on(
comment::creator_id
.eq(item_creator)
// Need to filter out the post rows where the post_id given is null
// Otherwise you'll get duped post rows
.or(
post::creator_id
.eq(item_creator)
.and(person_saved_combined::post_id.is_not_null()),
),
),
)
// The community
.inner_join(community::table.on(post::community_id.eq(community::id)))
.left_join(actions_alias(
creator_community_actions,
item_creator,
post::community_id,
))
.left_join(
local_user::table.on(
item_creator
.eq(local_user::person_id)
.and(local_user::admin.eq(true)),
),
)
.left_join(actions(
community_actions::table,
Some(my_person_id),
post::community_id,
))
.left_join(actions(post_actions::table, Some(my_person_id), post::id))
.left_join(actions(
person_actions::table,
Some(my_person_id),
item_creator,
))
.inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id)))
.left_join(
comment_aggregates::table
.on(person_saved_combined::comment_id.eq(comment_aggregates::comment_id.nullable())),
)
.left_join(actions(
comment_actions::table,
Some(my_person_id),
comment::id,
))
.left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable())))
// The person id filter
.filter(person_saved_combined::person_id.eq(my_person_id))
.select((
// Post-specific
post_aggregates::all_columns,
coalesce(
post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(),
post_aggregates::comments,
),
post_actions::saved.nullable().is_not_null(),
post_actions::read.nullable().is_not_null(),
post_actions::hidden.nullable().is_not_null(),
post_actions::like_score.nullable(),
image_details::all_columns.nullable(),
// Comment-specific
comment::all_columns.nullable(),
comment_aggregates::all_columns.nullable(),
comment_actions::saved.nullable().is_not_null(),
comment_actions::like_score.nullable(),
// Shared
post::all_columns,
community::all_columns,
person::all_columns,
CommunityFollower::select_subscribed_type(),
local_user::admin.nullable().is_not_null(),
creator_community_actions
.field(community_actions::became_moderator)
.nullable()
.is_not_null(),
creator_community_actions
.field(community_actions::received_ban)
.nullable()
.is_not_null(),
person_actions::blocked.nullable().is_not_null(),
community_actions::received_ban.nullable().is_not_null(),
))
.into_boxed();
// let mut query = PaginatedQueryBuilder::new(query);
let mut query = PaginatedQueryBuilder::new(query);
// let page_after = self.page_after.map(|c| c.0);
let page_after = self.page_after.map(|c| c.0);
// if self.page_back.unwrap_or_default() {
// query = query.before(page_after).limit_and_offset_from_end();
// } else {
// query = query.after(page_after);
// }
if self.page_back.unwrap_or_default() {
query = query.before(page_after).limit_and_offset_from_end();
} else {
query = query.after(page_after);
}
// // Sorting by published
// query = query
// .then_desc(ReverseTimestampKey(key::published))
// // Tie breaker
// .then_desc(key::id);
// Sorting by published
query = query
.then_desc(key::published)
// Tie breaker
.then_desc(key::id);
// let res = query.load::<PersonContentViewInternal>(conn).await?;
let res = query.load::<PersonContentViewInternal>(conn).await?;
// // Map the query results to the enum
// let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect();
// Map the query results to the enum
let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect();
// Ok(out)
// }
// }
Ok(out)
}
}
// impl InternalToCombinedView for PersonContentViewInternal {
// type CombinedView = PersonContentCombinedView;
#[cfg(test)]
#[expect(clippy::indexing_slicing)]
mod tests {
// fn map_to_enum(&self) -> Option<Self::CombinedView> {
// // Use for a short alias
// let v = self.clone();
use crate::{
person_saved_combined_view::PersonSavedCombinedQuery,
structs::{LocalUserView, PersonContentCombinedView},
};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentInsertForm, CommentSaved, CommentSavedForm},
community::{Community, CommunityInsertForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm, PostSaved, PostSavedForm},
},
traits::{Crud, Saveable},
utils::{build_db_pool_for_tests, DbPool},
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
// if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) {
// Some(PersonContentCombinedView::Comment(CommentView {
// comment,
// counts,
// post: v.post,
// community: v.community,
// creator: v.item_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,
// creator_blocked: v.item_creator_blocked,
// subscribed: v.subscribed,
// saved: v.comment_saved,
// my_vote: v.my_comment_vote,
// banned_from_community: v.banned_from_community,
// }))
// } else {
// Some(PersonContentCombinedView::Post(PostView {
// post: v.post,
// community: v.community,
// unread_comments: v.post_unread_comments,
// counts: v.post_counts,
// creator: v.item_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,
// creator_blocked: v.item_creator_blocked,
// subscribed: v.subscribed,
// saved: v.post_saved,
// read: v.post_read,
// hidden: v.post_hidden,
// my_vote: v.my_post_vote,
// image_details: v.image_details,
// banned_from_community: v.banned_from_community,
// }))
// }
// }
// }
struct Data {
instance: Instance,
timmy: Person,
timmy_view: LocalUserView,
sara: Person,
timmy_post: Post,
sara_comment: Comment,
sara_comment_2: Comment,
}
// #[cfg(test)]
// #[expect(clippy::indexing_slicing)]
// mod tests {
async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
// use crate::{
// profile_combined_view::ProfileCombinedQuery,
// report_combined_view::ReportCombinedQuery,
// structs::{
// CommentReportView,
// LocalUserView,
// PostReportView,
// PersonContentCombinedView,
// ReportCombinedView,
// ReportCombinedViewInternal,
// },
// };
// use lemmy_db_schema::{
// aggregates::structs::{CommentAggregates, PostAggregates},
// assert_length,
// source::{
// comment::{Comment, CommentInsertForm, CommentSaved, CommentSavedForm},
// comment_report::{CommentReport, CommentReportForm},
// community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
// instance::Instance,
// local_user::{LocalUser, LocalUserInsertForm},
// local_user_vote_display_mode::LocalUserVoteDisplayMode,
// person::{Person, PersonInsertForm},
// post::{Post, PostInsertForm},
// post_report::{PostReport, PostReportForm},
// private_message::{PrivateMessage, PrivateMessageInsertForm},
// private_message_report::{PrivateMessageReport, PrivateMessageReportForm},
// },
// traits::{Crud, Joinable, Reportable, Saveable},
// utils::{build_db_pool_for_tests, DbPool},
// };
// use lemmy_utils::error::LemmyResult;
// use pretty_assertions::assert_eq;
// use serial_test::serial;
let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv");
let timmy = Person::create(pool, &timmy_form).await?;
let timmy_local_user_form = LocalUserInsertForm::test_form(timmy.id);
let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?;
let timmy_view = LocalUserView {
local_user: timmy_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
person: timmy.clone(),
counts: Default::default(),
};
// struct Data {
// instance: Instance,
// timmy: Person,
// sara: Person,
// timmy_view: LocalUserView,
// community: Community,
// timmy_post: Post,
// timmy_post_2: Post,
// sara_post: Post,
// timmy_comment: Comment,
// sara_comment: Comment,
// sara_comment_2: Comment,
// }
let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv");
let sara = Person::create(pool, &sara_form).await?;
// async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
// let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let community_form = CommunityInsertForm::new(
instance.id,
"test community pcv".to_string(),
"nada".to_owned(),
"pubkey".to_string(),
);
let community = Community::create(pool, &community_form).await?;
// let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_pcv");
// let inserted_timmy = Person::create(pool, &timmy_form).await?;
// let timmy_local_user_form = LocalUserInsertForm::test_form(inserted_timmy.id);
// let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?;
// let timmy_view = LocalUserView {
// local_user: timmy_local_user,
// local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
// person: inserted_timmy.clone(),
// counts: Default::default(),
// };
let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id);
let timmy_post = Post::create(pool, &timmy_post_form).await?;
// let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_pcv");
// let inserted_sara = Person::create(pool, &sara_form).await?;
let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, community.id);
let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?;
// let community_form = CommunityInsertForm::new(
// inserted_instance.id,
// "test community pcv".to_string(),
// "nada".to_owned(),
// "pubkey".to_string(),
// );
// let inserted_community = Community::create(pool, &community_form).await?;
let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community.id);
let _sara_post = Post::create(pool, &sara_post_form).await?;
// let timmy_post_form = PostInsertForm::new(
// "timmy post prv".into(),
// inserted_timmy.id,
// inserted_community.id,
// );
// let timmy_post = Post::create(pool, &timmy_post_form).await?;
let timmy_comment_form =
CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into());
let _timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?;
// let timmy_post_form_2 = PostInsertForm::new(
// "timmy post prv 2".into(),
// inserted_timmy.id,
// inserted_community.id,
// );
// let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?;
let sara_comment_form =
CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into());
let sara_comment = Comment::create(pool, &sara_comment_form, None).await?;
// let sara_post_form = PostInsertForm::new(
// "sara post prv".into(),
// inserted_sara.id,
// inserted_community.id,
// );
// let sara_post = Post::create(pool, &sara_post_form).await?;
let sara_comment_form_2 =
CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into());
let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?;
// let timmy_comment_form =
// CommentInsertForm::new(inserted_timmy.id, timmy_post.id, "timmy comment prv".into());
// let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?;
Ok(Data {
instance,
timmy,
timmy_view,
sara,
timmy_post,
sara_comment,
sara_comment_2,
})
}
// let sara_comment_form =
// CommentInsertForm::new(inserted_sara.id, timmy_post.id, "sara comment prv".into());
// let sara_comment = Comment::create(pool, &sara_comment_form, None).await?;
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
Instance::delete(pool, data.instance.id).await?;
// let sara_comment_form_2 = CommentInsertForm::new(
// inserted_sara.id,
// timmy_post_2.id,
// "sara comment prv 2".into(),
// );
// let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?;
Ok(())
}
// Ok(Data {
// instance: inserted_instance,
// timmy: inserted_timmy,
// sara: inserted_sara,
// timmy_view,
// community: inserted_community,
// timmy_post,
// timmy_post_2,
// sara_post,
// timmy_comment,
// sara_comment,
// sara_comment_2,
// })
// }
#[tokio::test]
#[serial]
async fn test_combined() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
// async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
// Instance::delete(pool, data.instance.id).await?;
// Do a batch read of timmy saved
let timmy_saved = PersonSavedCombinedQuery::default()
.list(pool, &data.timmy_view)
.await?;
assert_eq!(0, timmy_saved.len());
// Ok(())
// }
// Save a few things
let save_sara_comment_2 =
CommentSavedForm::new(data.sara_comment_2.id, data.timmy_view.person.id);
CommentSaved::save(pool, &save_sara_comment_2).await?;
// #[tokio::test]
// #[serial]
// async fn test_combined() -> LemmyResult<()> {
// let pool = &build_db_pool_for_tests();
// let pool = &mut pool.into();
// let data = init_data(pool).await?;
let save_sara_comment = CommentSavedForm::new(data.sara_comment.id, data.timmy_view.person.id);
CommentSaved::save(pool, &save_sara_comment).await?;
// // Do a batch read of timmy
// let timmy_content = ProfileCombinedQuery::default().list(pool, &None).await?;
// assert_eq!(3, timmy_content.len());
let post_save_form = PostSavedForm::new(data.timmy_post.id, data.timmy.id);
PostSaved::save(pool, &post_save_form).await?;
// // Make sure the report types are correct
// if let PersonContentCombinedView::Comment(v) = &timmy_content[0] {
// assert_eq!(data.timmy_comment.id, v.comment.id);
// assert_eq!(data.timmy.id, v.creator.id);
// } else {
// panic!("wrong type");
// }
// if let PersonContentCombinedView::Post(v) = &timmy_content[1] {
// assert_eq!(data.timmy_post_2.id, v.post.id);
// assert_eq!(data.timmy.id, v.post.creator_id);
// } else {
// panic!("wrong type");
// }
// if let PersonContentCombinedView::Post(v) = &timmy_content[2] {
// assert_eq!(data.timmy_post.id, v.post.id);
// assert_eq!(data.timmy.id, v.post.creator_id);
// } else {
// panic!("wrong type");
// }
let timmy_saved = PersonSavedCombinedQuery::default()
.list(pool, &data.timmy_view)
.await?;
assert_eq!(3, timmy_saved.len());
// // Do a batch read of sara
// let sara_content = ProfileCombinedQuery::default().list(pool, &None).await?;
// assert_eq!(3, sara_content.len());
// Make sure the types and order are correct
if let PersonContentCombinedView::Post(v) = &timmy_saved[0] {
assert_eq!(data.timmy_post.id, v.post.id);
assert_eq!(data.timmy.id, v.post.creator_id);
} else {
panic!("wrong type");
}
if let PersonContentCombinedView::Comment(v) = &timmy_saved[1] {
assert_eq!(data.sara_comment.id, v.comment.id);
assert_eq!(data.sara.id, v.comment.creator_id);
} else {
panic!("wrong type");
}
if let PersonContentCombinedView::Comment(v) = &timmy_saved[2] {
assert_eq!(data.sara_comment_2.id, v.comment.id);
assert_eq!(data.sara.id, v.comment.creator_id);
} else {
panic!("wrong type");
}
// // Make sure the report types are correct
// if let PersonContentCombinedView::Comment(v) = &sara_content[0] {
// assert_eq!(data.sara_comment_2.id, v.comment.id);
// assert_eq!(data.sara.id, v.creator.id);
// // This one was to timmy_post_2
// assert_eq!(data.timmy_post_2.id, v.post.id);
// assert_eq!(data.timmy.id, v.post.creator_id);
// } else {
// panic!("wrong type");
// }
// if let PersonContentCombinedView::Comment(v) = &sara_content[1] {
// assert_eq!(data.sara_comment.id, v.comment.id);
// assert_eq!(data.sara.id, v.creator.id);
// assert_eq!(data.timmy_post.id, v.post.id);
// assert_eq!(data.timmy.id, v.post.creator_id);
// } else {
// panic!("wrong type");
// }
// if let PersonContentCombinedView::Post(v) = &sara_content[2] {
// assert_eq!(data.timmy_post.id, v.post.id);
// assert_eq!(data.timmy.id, v.post.creator_id);
// } else {
// panic!("wrong type");
// }
// Try unsaving 2 things
CommentSaved::unsave(pool, &save_sara_comment).await?;
PostSaved::unsave(pool, &post_save_form).await?;
// // Timmy saves sara's comment, and his 2nd post
// let save_comment_0_form = CommentSavedForm {
// person_id: data.timmy.id,
// comment_id: data.sara_comment.id,
// };
// CommentSaved::save(pool, &save_comment_0_form).await?;
let timmy_saved = PersonSavedCombinedQuery::default()
.list(pool, &data.timmy_view)
.await?;
assert_eq!(1, timmy_saved.len());
// // Timmy saves sara's comment, and his 2nd post
// let save_comment_0_form = CommentSavedForm {
// person_id: data.timmy.id,
// comment_id: data.sara_comment.id,
// };
// CommentSaved::save(pool, &save_comment_0_form).await?;
if let PersonContentCombinedView::Comment(v) = &timmy_saved[0] {
assert_eq!(data.sara_comment_2.id, v.comment.id);
assert_eq!(data.sara.id, v.comment.creator_id);
} else {
panic!("wrong type");
}
// // Do a saved_only query
// let timmy_content_saved_only = ProfileCombinedQuery {}.list(pool, &None).await?;
cleanup(data, pool).await?;
// cleanup(data, pool).await?;
// Ok(())
// }
// }
// #[tokio::test]
// #[serial]
// async fn test_saved_order() -> LemmyResult<()> {
// let pool = &build_db_pool_for_tests();
// let pool = &mut pool.into();
// let data = init_data(pool).await?;
// // Save two comments
// let save_comment_0_form = CommentSavedForm {
// person_id: data.timmy_local_user_view.person.id,
// comment_id: data.inserted_comment_0.id,
// };
// CommentSaved::save(pool, &save_comment_0_form).await?;
// let save_comment_2_form = CommentSavedForm {
// person_id: data.timmy_local_user_view.person.id,
// comment_id: data.inserted_comment_2.id,
// };
// CommentSaved::save(pool, &save_comment_2_form).await?;
// // Fetch the saved comments
// let comments = CommentQuery {
// local_user: Some(&data.timmy_local_user_view.local_user),
// saved_only: Some(true),
// ..Default::default()
// }
// .list(&data.site, pool)
// .await?;
// // There should only be two comments
// assert_eq!(2, comments.len());
// // The first comment, should be the last one saved (descending order)
// assert_eq!(comments[0].comment.id, data.inserted_comment_2.id);
// // The second comment, should be the first one saved
// assert_eq!(comments[1].comment.id, data.inserted_comment_0.id);
// cleanup(data, pool).await
// }
// #[tokio::test]
// #[serial]
// async fn post_listing_saved_only() -> LemmyResult<()> {
// let pool = &build_db_pool()?;
// let pool = &mut pool.into();
// let data = init_data(pool).await?;
// // Save only the bot post
// // The saved_only should only show the bot post
// let post_save_form =
// PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id);
// PostSaved::save(pool, &post_save_form).await?;
// // Read the saved only
// let read_saved_post_listing = PostQuery {
// community_id: Some(data.inserted_community.id),
// saved_only: Some(true),
// ..data.default_post_query()
// }
// .list(&data.site, pool)
// .await?;
// // This should only include the bot post, not the one you created
// assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing));
// cleanup(data, pool).await
// }
Ok(())
}
}

View file

@ -1,3 +1,4 @@
DROP TABLE person_content_combined;
DROP TABLE person_saved_combined;

View file

@ -33,6 +33,7 @@ FROM
CREATE TABLE person_saved_combined (
id serial PRIMARY KEY,
published timestamptz NOT NULL,
person_id int NOT NULL REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE,
post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE,
comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE,
-- Make sure only one of the columns is not null
@ -43,23 +44,26 @@ CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (publi
CREATE INDEX idx_person_saved_combined_published_asc ON person_saved_combined (reverse_timestamp_sort (published) DESC, id DESC);
CREATE INDEX idx_person_saved_combined ON person_saved_combined (person_id);
-- Updating the history
INSERT INTO person_saved_combined (published, post_id)
INSERT INTO person_saved_combined (published, person_id, post_id)
SELECT
saved,
person_id,
post_id
FROM
post_actions
WHERE
WHERE
saved IS NOT NULL;
INSERT INTO person_saved_combined (published, comment_id)
INSERT INTO person_saved_combined (published, person_id, comment_id)
SELECT
saved,
person_id,
comment_id
FROM
comment_actions
WHERE
WHERE
saved IS NOT NULL;

View file

@ -31,6 +31,7 @@ use lemmy_api::{
list_banned::list_banned_users,
list_logins::list_logins,
list_media::list_media,
list_saved::list_person_saved,
login::login,
logout::logout,
notifications::{
@ -341,7 +342,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
.route("", get().to(read_person))
.route("/content", get().to(list_person_content))
// TODO move this to /account/saved after http routes
// .route("/saved", get().to(read_person_saved))
.route("/saved", get().to(list_person_saved))
.route("/mention", get().to(list_mentions))
.route(
"/mention/mark_as_read",