mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-09-03 03:33:50 +00:00
Keep totals of upvotes and downvotes given to each user. (#5786)
* Keep totals of upvotes and downvotes given to each user. - Adds a `voted_at`, `upvotes` and `downvotes` to person_actions. - Didn't use triggers, because I couldn't figure out how to get the voter, and also because it should only be for local users anyway. - Fixes #2370 * Clippy * Adding history filling. * Try to fix postgres alias error 1 * Update crates/db_schema/src/impls/person.rs Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com> * Addressing PR comments * Removing commented line. * Fixing type errors. * Fixing test * Fixing like score 0 and api test errors. * Some clippy fixes * Update migrations/2025-06-14-141408_person_votes/up.sql Co-authored-by: dullbananas <dull.bananas0@gmail.com> * Update migrations/2025-06-14-141408_person_votes/up.sql Co-authored-by: dullbananas <dull.bananas0@gmail.com> * Update migrations/2025-06-14-141408_person_votes/up.sql Co-authored-by: dullbananas <dull.bananas0@gmail.com> * Formatting sql * Fixing migration. * Cleaning up merge --------- Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com> Co-authored-by: dullbananas <dull.bananas0@gmail.com>
This commit is contained in:
parent
d274d2bb1d
commit
a6b03fabdb
18 changed files with 453 additions and 62 deletions
|
@ -189,6 +189,36 @@ test("Unlike a post", async () => {
|
|||
await assertPostFederation(betaPost, postRes.post_view);
|
||||
});
|
||||
|
||||
test("Make sure like is within range", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let postRes = await createPost(alpha, betaCommunity.community.id);
|
||||
|
||||
// Try a like with score 2
|
||||
await expect(
|
||||
likePost(alpha, 2, postRes.post_view.post),
|
||||
).rejects.toStrictEqual(new LemmyError("couldnt_like_post"));
|
||||
|
||||
// Try a like with score -2
|
||||
await expect(
|
||||
likePost(alpha, -2, postRes.post_view.post),
|
||||
).rejects.toStrictEqual(new LemmyError("couldnt_like_post"));
|
||||
|
||||
// Make sure that post stayed at 1
|
||||
const betaPost = await waitForPost(
|
||||
beta,
|
||||
postRes.post_view.post,
|
||||
post => post?.post.score === 1,
|
||||
);
|
||||
|
||||
expect(betaPost).toBeDefined();
|
||||
expect(betaPost?.community.local).toBe(true);
|
||||
expect(betaPost?.creator.local).toBe(false);
|
||||
expect(betaPost?.post.score).toBe(1);
|
||||
await assertPostFederation(betaPost, postRes.post_view);
|
||||
});
|
||||
|
||||
test("Update a post", async () => {
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
|
|
|
@ -12,6 +12,7 @@ use lemmy_db_schema::{
|
|||
source::{
|
||||
comment::{CommentActions, CommentLikeForm},
|
||||
comment_reply::CommentReply,
|
||||
person::PersonActions,
|
||||
},
|
||||
traits::Likeable,
|
||||
};
|
||||
|
@ -32,6 +33,7 @@ pub async fn like_comment(
|
|||
let local_site = SiteView::read_local(&mut context.pool()).await?.local_site;
|
||||
let local_instance_id = local_user_view.person.instance_id;
|
||||
let comment_id = data.comment_id;
|
||||
let my_person_id = local_user_view.person.id;
|
||||
|
||||
let mut recipient_ids = Vec::<LocalUserId>::new();
|
||||
|
||||
|
@ -39,7 +41,7 @@ pub async fn like_comment(
|
|||
data.score,
|
||||
PostOrCommentId::Comment(comment_id),
|
||||
&local_site,
|
||||
local_user_view.person.id,
|
||||
my_person_id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
@ -52,6 +54,7 @@ pub async fn like_comment(
|
|||
local_instance_id,
|
||||
)
|
||||
.await?;
|
||||
let previous_score = orig_comment.comment_actions.and_then(|p| p.like_score);
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view,
|
||||
|
@ -70,19 +73,33 @@ pub async fn like_comment(
|
|||
}
|
||||
}
|
||||
|
||||
let mut like_form = CommentLikeForm::new(local_user_view.person.id, data.comment_id, data.score);
|
||||
let mut like_form = CommentLikeForm::new(my_person_id, data.comment_id, data.score);
|
||||
|
||||
// Remove any likes first
|
||||
let person_id = local_user_view.person.id;
|
||||
CommentActions::remove_like(&mut context.pool(), my_person_id, comment_id).await?;
|
||||
if let Some(previous_score) = previous_score {
|
||||
PersonActions::remove_like(
|
||||
&mut context.pool(),
|
||||
my_person_id,
|
||||
orig_comment.creator.id,
|
||||
previous_score,
|
||||
)
|
||||
.await
|
||||
// Ignore errors, since a previous_like of zero throws an error
|
||||
.ok();
|
||||
}
|
||||
|
||||
CommentActions::remove_like(&mut context.pool(), person_id, comment_id).await?;
|
||||
|
||||
// Only add the like if the score isnt 0
|
||||
let do_add =
|
||||
like_form.like_score != 0 && (like_form.like_score == 1 || like_form.like_score == -1);
|
||||
if do_add {
|
||||
if like_form.like_score != 0 {
|
||||
like_form = plugin_hook_before("before_comment_vote", like_form).await?;
|
||||
let like = CommentActions::like(&mut context.pool(), &like_form).await?;
|
||||
PersonActions::like(
|
||||
&mut context.pool(),
|
||||
my_person_id,
|
||||
orig_comment.creator.id,
|
||||
like_form.like_score,
|
||||
)
|
||||
.await?;
|
||||
|
||||
plugin_hook_after("after_comment_vote", &like)?;
|
||||
}
|
||||
|
||||
|
@ -91,7 +108,7 @@ pub async fn like_comment(
|
|||
object_id: orig_comment.comment.ap_id,
|
||||
actor: local_user_view.person.clone(),
|
||||
community: orig_comment.community,
|
||||
previous_score: orig_comment.comment_actions.and_then(|a| a.like_score),
|
||||
previous_score,
|
||||
new_score: data.score,
|
||||
},
|
||||
&context,
|
||||
|
|
|
@ -164,6 +164,7 @@ pub async fn save_user_settings(
|
|||
show_upvotes: data.show_upvotes,
|
||||
show_downvotes: data.show_downvotes,
|
||||
show_upvote_percentage: data.show_upvote_percentage,
|
||||
show_person_votes: data.show_person_votes,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,10 @@ use lemmy_api_utils::{
|
|||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::PostOrCommentId,
|
||||
source::post::{PostActions, PostLikeForm, PostReadForm},
|
||||
source::{
|
||||
person::PersonActions,
|
||||
post::{PostActions, PostLikeForm, PostReadForm},
|
||||
},
|
||||
traits::{Likeable, Readable},
|
||||
};
|
||||
use lemmy_db_views_local_user::LocalUserView;
|
||||
|
@ -29,12 +32,13 @@ pub async fn like_post(
|
|||
let local_site = SiteView::read_local(&mut context.pool()).await?.local_site;
|
||||
let local_instance_id = local_user_view.person.instance_id;
|
||||
let post_id = data.post_id;
|
||||
let my_person_id = local_user_view.person.id;
|
||||
|
||||
check_local_vote_mode(
|
||||
data.score,
|
||||
PostOrCommentId::Post(post_id),
|
||||
&local_site,
|
||||
local_user_view.person.id,
|
||||
my_person_id,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
@ -43,27 +47,42 @@ pub async fn like_post(
|
|||
// Check for a community ban
|
||||
let orig_post =
|
||||
PostView::read(&mut context.pool(), post_id, None, local_instance_id, false).await?;
|
||||
let previous_score = orig_post.post_actions.and_then(|p| p.like_score);
|
||||
|
||||
check_community_user_action(&local_user_view, &orig_post.community, &mut context.pool()).await?;
|
||||
|
||||
let mut like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score);
|
||||
let mut like_form = PostLikeForm::new(data.post_id, my_person_id, data.score);
|
||||
|
||||
// Remove any likes first
|
||||
let person_id = local_user_view.person.id;
|
||||
PostActions::remove_like(&mut context.pool(), my_person_id, post_id).await?;
|
||||
if let Some(previous_score) = previous_score {
|
||||
PersonActions::remove_like(
|
||||
&mut context.pool(),
|
||||
my_person_id,
|
||||
orig_post.creator.id,
|
||||
previous_score,
|
||||
)
|
||||
.await
|
||||
// Ignore errors, since a previous_like of zero throws an error
|
||||
.ok();
|
||||
}
|
||||
|
||||
PostActions::remove_like(&mut context.pool(), person_id, post_id).await?;
|
||||
|
||||
// Only add the like if the score isnt 0
|
||||
let do_add =
|
||||
like_form.like_score != 0 && (like_form.like_score == 1 || like_form.like_score == -1);
|
||||
if do_add {
|
||||
if like_form.like_score != 0 {
|
||||
like_form = plugin_hook_before("before_post_vote", like_form).await?;
|
||||
let like = PostActions::like(&mut context.pool(), &like_form).await?;
|
||||
PersonActions::like(
|
||||
&mut context.pool(),
|
||||
my_person_id,
|
||||
orig_post.creator.id,
|
||||
like_form.like_score,
|
||||
)
|
||||
.await?;
|
||||
|
||||
plugin_hook_after("after_post_vote", &like)?;
|
||||
}
|
||||
|
||||
// Mark Post Read
|
||||
let read_form = PostReadForm::new(post_id, person_id);
|
||||
let read_form = PostReadForm::new(post_id, my_person_id);
|
||||
PostActions::mark_as_read(&mut context.pool(), &read_form).await?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
|
@ -71,7 +90,7 @@ pub async fn like_post(
|
|||
object_id: orig_post.post.ap_id,
|
||||
actor: local_user_view.person.clone(),
|
||||
community: orig_post.community.clone(),
|
||||
previous_score: orig_post.post_actions.and_then(|a| a.like_score),
|
||||
previous_score,
|
||||
new_score: data.score,
|
||||
},
|
||||
&context,
|
||||
|
|
|
@ -62,7 +62,6 @@ pub(crate) async fn to_local_url(url: &str, context: &Data<LemmyContext>) -> Opt
|
|||
Right(multi) => multi.format_url(context.settings()),
|
||||
}
|
||||
.ok()
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -14,6 +14,7 @@ use crate::{
|
|||
functions::{coalesce, hot_rank},
|
||||
get_conn,
|
||||
uplete,
|
||||
validate_like,
|
||||
DbPool,
|
||||
DELETED_REPLACEMENT_TEXT,
|
||||
},
|
||||
|
@ -31,7 +32,7 @@ use diesel_async::RunQueryDsl;
|
|||
use diesel_ltree::Ltree;
|
||||
use lemmy_db_schema_file::schema::{comment, comment_actions, community, post};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
|
||||
settings::structs::Settings,
|
||||
};
|
||||
use url::Url;
|
||||
|
@ -269,6 +270,9 @@ impl Likeable for CommentActions {
|
|||
|
||||
async fn like(pool: &mut DbPool<'_>, form: &Self::Form) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
validate_like(form.like_score).with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
|
||||
|
||||
insert_into(comment_actions::table)
|
||||
.values(form)
|
||||
.on_conflict((comment_actions::comment_id, comment_actions::person_id))
|
||||
|
|
|
@ -370,6 +370,84 @@ impl PersonActions {
|
|||
.await
|
||||
.with_lemmy_type(LemmyErrorType::NotFound)
|
||||
}
|
||||
|
||||
pub async fn like(
|
||||
pool: &mut DbPool<'_>,
|
||||
person_id: PersonId,
|
||||
target_id: PersonId,
|
||||
like_score: i16,
|
||||
) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
let (upvotes_inc, downvotes_inc) = match like_score {
|
||||
1 => (1, 0),
|
||||
-1 => (0, 1),
|
||||
_ => return Err(LemmyErrorType::NotFound.into()),
|
||||
};
|
||||
|
||||
let voted_at = Utc::now();
|
||||
|
||||
insert_into(person_actions::table)
|
||||
.values((
|
||||
person_actions::person_id.eq(person_id),
|
||||
person_actions::target_id.eq(target_id),
|
||||
person_actions::voted_at.eq(voted_at),
|
||||
person_actions::upvotes.eq(upvotes_inc),
|
||||
person_actions::downvotes.eq(downvotes_inc),
|
||||
))
|
||||
.on_conflict((person_actions::person_id, person_actions::target_id))
|
||||
.do_update()
|
||||
.set((
|
||||
person_actions::person_id.eq(person_id),
|
||||
person_actions::target_id.eq(target_id),
|
||||
person_actions::voted_at.eq(voted_at),
|
||||
person_actions::upvotes.eq(person_actions::upvotes + upvotes_inc),
|
||||
person_actions::downvotes.eq(person_actions::downvotes + downvotes_inc),
|
||||
))
|
||||
.returning(Self::as_select())
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::NotFound)
|
||||
}
|
||||
|
||||
/// Removes a person like. A previous_score of zero throws an error.
|
||||
pub async fn remove_like(
|
||||
pool: &mut DbPool<'_>,
|
||||
person_id: PersonId,
|
||||
target_id: PersonId,
|
||||
previous_score: i16,
|
||||
) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
let (upvotes_inc, downvotes_inc) = match previous_score {
|
||||
1 => (-1, 0),
|
||||
-1 => (0, -1),
|
||||
_ => return Err(LemmyErrorType::NotFound.into()),
|
||||
};
|
||||
let voted_at = Utc::now();
|
||||
|
||||
insert_into(person_actions::table)
|
||||
.values((
|
||||
person_actions::person_id.eq(person_id),
|
||||
person_actions::target_id.eq(target_id),
|
||||
person_actions::voted_at.eq(voted_at),
|
||||
person_actions::upvotes.eq(upvotes_inc),
|
||||
person_actions::downvotes.eq(downvotes_inc),
|
||||
))
|
||||
.on_conflict((person_actions::person_id, person_actions::target_id))
|
||||
.do_update()
|
||||
.set((
|
||||
person_actions::person_id.eq(person_id),
|
||||
person_actions::target_id.eq(target_id),
|
||||
person_actions::voted_at.eq(voted_at),
|
||||
person_actions::upvotes.eq(person_actions::upvotes + upvotes_inc),
|
||||
person_actions::downvotes.eq(person_actions::downvotes + downvotes_inc),
|
||||
))
|
||||
.returning(Self::as_select())
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -17,6 +17,7 @@ use crate::{
|
|||
get_conn,
|
||||
now,
|
||||
uplete,
|
||||
validate_like,
|
||||
DbPool,
|
||||
DELETED_REPLACEMENT_TEXT,
|
||||
FETCH_LIMIT_MAX,
|
||||
|
@ -40,7 +41,7 @@ use diesel::{
|
|||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema_file::schema::{community, person, post, post_actions};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
error::{LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
|
||||
settings::structs::Settings,
|
||||
};
|
||||
|
||||
|
@ -345,6 +346,9 @@ impl Likeable for PostActions {
|
|||
|
||||
async fn like(pool: &mut DbPool<'_>, form: &Self::Form) -> LemmyResult<Self> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
validate_like(form.like_score).with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
|
||||
|
||||
insert_into(post_actions::table)
|
||||
.values(form)
|
||||
.on_conflict((post_actions::post_id, post_actions::person_id))
|
||||
|
@ -430,7 +434,7 @@ impl Readable for PostActions {
|
|||
type Form = PostReadForm;
|
||||
|
||||
async fn mark_as_read(pool: &mut DbPool<'_>, form: &Self::Form) -> LemmyResult<usize> {
|
||||
Self::mark_many_as_read(pool, &[form.clone()]).await
|
||||
Self::mark_many_as_read(pool, std::slice::from_ref(form)).await
|
||||
}
|
||||
|
||||
async fn mark_as_unread(pool: &mut DbPool<'_>, form: &Self::Form) -> LemmyResult<uplete::Count> {
|
||||
|
|
|
@ -83,6 +83,7 @@ pub struct LocalUser {
|
|||
pub show_upvotes: bool,
|
||||
pub show_downvotes: VoteShow,
|
||||
pub show_upvote_percentage: bool,
|
||||
pub show_person_votes: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, derive_new::new)]
|
||||
|
@ -155,6 +156,8 @@ pub struct LocalUserInsertForm {
|
|||
pub show_downvotes: Option<VoteShow>,
|
||||
#[new(default)]
|
||||
pub show_upvote_percentage: Option<bool>,
|
||||
#[new(default)]
|
||||
pub show_person_votes: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
@ -194,4 +197,5 @@ pub struct LocalUserUpdateForm {
|
|||
pub show_upvotes: Option<bool>,
|
||||
pub show_downvotes: Option<VoteShow>,
|
||||
pub show_upvote_percentage: Option<bool>,
|
||||
pub show_person_votes: Option<bool>,
|
||||
}
|
||||
|
|
|
@ -148,6 +148,12 @@ pub struct PersonActions {
|
|||
pub noted_at: Option<DateTime<Utc>>,
|
||||
/// A note about the person.
|
||||
pub note: Option<String>,
|
||||
/// When the person was voted on.
|
||||
pub voted_at: Option<DateTime<Utc>>,
|
||||
/// A total of upvotes given to this person
|
||||
pub upvotes: Option<i32>,
|
||||
/// A total of downvotes given to this person
|
||||
pub downvotes: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, derive_new::new)]
|
||||
|
|
|
@ -399,7 +399,7 @@ fn build_config_options_uri_segment(config: &str) -> LemmyResult<String> {
|
|||
Ok(url.into())
|
||||
}
|
||||
|
||||
fn establish_connection(config: &str) -> BoxFuture<ConnectionResult<AsyncPgConnection>> {
|
||||
fn establish_connection(config: &str) -> BoxFuture<'_, ConnectionResult<AsyncPgConnection>> {
|
||||
let fut = async {
|
||||
/// Use a once_lock to create the postgres connection config, since this config never changes
|
||||
static POSTGRES_CONFIG_WITH_OPTIONS: OnceLock<String> = OnceLock::new();
|
||||
|
@ -640,6 +640,18 @@ pub(crate) fn format_actor_url(
|
|||
Ok(Url::parse(&url)?)
|
||||
}
|
||||
|
||||
/// Make sure the like score is 1, or -1
|
||||
///
|
||||
/// Uses a default NotFound error, that you should map to
|
||||
/// CouldntLikeComment/CouldntLikePost.
|
||||
pub(crate) fn validate_like(like_score: i16) -> LemmyResult<()> {
|
||||
if [-1, 1].contains(&like_score) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LemmyErrorType::NotFound.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -532,6 +532,7 @@ diesel::table! {
|
|||
show_upvotes -> Bool,
|
||||
show_downvotes -> VoteShowEnum,
|
||||
show_upvote_percentage -> Bool,
|
||||
show_person_votes -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -744,7 +745,7 @@ diesel::table! {
|
|||
use diesel::sql_types::*;
|
||||
use super::sql_types::CommunityFollowerState;
|
||||
|
||||
multi_community_follow (multi_community_id, person_id) {
|
||||
multi_community_follow (person_id, multi_community_id) {
|
||||
multi_community_id -> Int4,
|
||||
person_id -> Int4,
|
||||
follow_state -> CommunityFollowerState,
|
||||
|
@ -831,6 +832,9 @@ diesel::table! {
|
|||
blocked_at -> Nullable<Timestamptz>,
|
||||
noted_at -> Nullable<Timestamptz>,
|
||||
note -> Nullable<Text>,
|
||||
voted_at -> Nullable<Timestamptz>,
|
||||
upvotes -> Nullable<Int4>,
|
||||
downvotes -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1059,7 +1063,7 @@ diesel::table! {
|
|||
comment_id -> Nullable<Int4>,
|
||||
community_id -> Nullable<Int4>,
|
||||
person_id -> Nullable<Int4>,
|
||||
multi_community_id -> Nullable<Int4>
|
||||
multi_community_id -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -150,7 +150,7 @@ async fn db_perf() -> LemmyResult<()> {
|
|||
|
||||
// TODO: include local_user
|
||||
let post_views = PostQuery {
|
||||
community_id: community_ids.as_slice().first().cloned().map(Into::into),
|
||||
community_id: community_ids.as_slice().first().cloned(),
|
||||
sort: Some(PostSortType::New),
|
||||
limit: Some(20),
|
||||
cursor_data,
|
||||
|
|
|
@ -624,8 +624,8 @@ mod tests {
|
|||
pool: ActualDbPool,
|
||||
instance: Instance,
|
||||
tegan: LocalUserView,
|
||||
john_local_user_view: LocalUserView,
|
||||
bot_local_user_view: LocalUserView,
|
||||
john: LocalUserView,
|
||||
bot: LocalUserView,
|
||||
community: Community,
|
||||
post: Post,
|
||||
bot_post: Post,
|
||||
|
@ -773,18 +773,18 @@ mod tests {
|
|||
];
|
||||
PostTag::set(pool, &inserted_tags).await?;
|
||||
|
||||
let tegan_local_user_view = LocalUserView {
|
||||
let tegan = LocalUserView {
|
||||
local_user: inserted_tegan_local_user,
|
||||
person: inserted_tegan_person,
|
||||
banned: false,
|
||||
};
|
||||
let john_local_user_view = LocalUserView {
|
||||
let john = LocalUserView {
|
||||
local_user: inserted_john_local_user,
|
||||
person: inserted_john_person,
|
||||
banned: false,
|
||||
};
|
||||
|
||||
let bot_local_user_view = LocalUserView {
|
||||
let bot = LocalUserView {
|
||||
local_user: inserted_bot_local_user,
|
||||
person: inserted_bot_person,
|
||||
banned: false,
|
||||
|
@ -793,9 +793,9 @@ mod tests {
|
|||
Ok(Data {
|
||||
pool: actual_pool,
|
||||
instance: data.instance,
|
||||
tegan: tegan_local_user_view,
|
||||
john_local_user_view,
|
||||
bot_local_user_view,
|
||||
tegan,
|
||||
john,
|
||||
bot,
|
||||
community,
|
||||
post,
|
||||
bot_post,
|
||||
|
@ -810,8 +810,8 @@ mod tests {
|
|||
let num_deleted = Post::delete(pool, data.post.id).await?;
|
||||
Community::delete(pool, data.community.id).await?;
|
||||
Person::delete(pool, data.tegan.person.id).await?;
|
||||
Person::delete(pool, data.bot_local_user_view.person.id).await?;
|
||||
Person::delete(pool, data.john_local_user_view.person.id).await?;
|
||||
Person::delete(pool, data.bot.person.id).await?;
|
||||
Person::delete(pool, data.john.person.id).await?;
|
||||
Site::delete(pool, data.site.id).await?;
|
||||
Instance::delete(pool, data.instance.id).await?;
|
||||
assert_eq!(1, num_deleted);
|
||||
|
@ -971,11 +971,13 @@ mod tests {
|
|||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
(true, 1, 1, 1),
|
||||
(true, true, 1, 1, 1),
|
||||
(
|
||||
post_listing_single_with_person
|
||||
.post_actions
|
||||
.is_some_and(|t| t.like_score == Some(1)),
|
||||
// Make sure person actions is none so you don't get a voted_at for your own user
|
||||
post_listing_single_with_person.person_actions.is_none(),
|
||||
post_listing_single_with_person.post.score,
|
||||
post_listing_single_with_person.post.upvotes,
|
||||
post_listing_single_with_person.creator.post_score,
|
||||
|
@ -1016,7 +1018,7 @@ mod tests {
|
|||
let note_str = "Tegan loves cats.";
|
||||
|
||||
let note_form = PersonNoteForm::new(
|
||||
data.john_local_user_view.person.id,
|
||||
data.john.person.id,
|
||||
data.tegan.person.id,
|
||||
note_str.to_string(),
|
||||
);
|
||||
|
@ -1026,7 +1028,7 @@ mod tests {
|
|||
let post_listing = PostView::read(
|
||||
pool,
|
||||
data.post.id,
|
||||
Some(&data.john_local_user_view.local_user),
|
||||
Some(&data.john.local_user),
|
||||
data.instance.id,
|
||||
false,
|
||||
)
|
||||
|
@ -1036,17 +1038,13 @@ mod tests {
|
|||
.person_actions
|
||||
.is_some_and(|t| t.note == Some(note_str.to_string()) && t.noted_at.is_some()));
|
||||
|
||||
let note_removed = PersonActions::delete_note(
|
||||
pool,
|
||||
data.john_local_user_view.person.id,
|
||||
data.tegan.person.id,
|
||||
)
|
||||
.await?;
|
||||
let note_removed =
|
||||
PersonActions::delete_note(pool, data.john.person.id, data.tegan.person.id).await?;
|
||||
|
||||
let post_listing = PostView::read(
|
||||
pool,
|
||||
data.post.id,
|
||||
Some(&data.john_local_user_view.local_user),
|
||||
Some(&data.john.local_user),
|
||||
data.instance.id,
|
||||
false,
|
||||
)
|
||||
|
@ -1058,6 +1056,158 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test_context(Data)]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn post_listing_person_vote_totals(data: &mut Data) -> LemmyResult<()> {
|
||||
let pool = &data.pool();
|
||||
let pool = &mut pool.into();
|
||||
|
||||
// Create a 2nd bot post, to do multiple votes
|
||||
let bot_post_2 = PostInsertForm::new(
|
||||
"Bot post 2".to_string(),
|
||||
data.bot.person.id,
|
||||
data.community.id,
|
||||
);
|
||||
let bot_post_2 = Post::create(pool, &bot_post_2).await?;
|
||||
|
||||
let post_like_form = PostLikeForm::new(data.bot_post.id, data.tegan.person.id, 1);
|
||||
let inserted_post_like = PostActions::like(pool, &post_like_form).await?;
|
||||
|
||||
assert_eq!(
|
||||
(data.bot_post.id, data.tegan.person.id, Some(1)),
|
||||
(
|
||||
inserted_post_like.post_id,
|
||||
inserted_post_like.person_id,
|
||||
inserted_post_like.like_score,
|
||||
)
|
||||
);
|
||||
|
||||
let inserted_person_like =
|
||||
PersonActions::like(pool, data.tegan.person.id, data.bot.person.id, 1).await?;
|
||||
|
||||
assert_eq!(
|
||||
(data.tegan.person.id, data.bot.person.id, Some(1), Some(0),),
|
||||
(
|
||||
inserted_person_like.person_id,
|
||||
inserted_person_like.target_id,
|
||||
inserted_person_like.upvotes,
|
||||
inserted_person_like.downvotes,
|
||||
)
|
||||
);
|
||||
|
||||
let post_listing = PostView::read(
|
||||
pool,
|
||||
data.bot_post.id,
|
||||
Some(&data.tegan.local_user),
|
||||
data.instance.id,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
(true, true, true, 1, 1, 1),
|
||||
(
|
||||
post_listing
|
||||
.post_actions
|
||||
.is_some_and(|t| t.like_score == Some(1)),
|
||||
post_listing
|
||||
.person_actions
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.upvotes == Some(1)),
|
||||
post_listing
|
||||
.person_actions
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.downvotes == Some(0)),
|
||||
post_listing.post.score,
|
||||
post_listing.post.upvotes,
|
||||
post_listing.creator.post_score,
|
||||
)
|
||||
);
|
||||
|
||||
// Do a 2nd like to another post
|
||||
let post_2_like_form = PostLikeForm::new(bot_post_2.id, data.tegan.person.id, 1);
|
||||
let _inserted_post_2_like = PostActions::like(pool, &post_2_like_form).await?;
|
||||
let inserted_person_like_2 =
|
||||
PersonActions::like(pool, data.tegan.person.id, data.bot.person.id, 1).await?;
|
||||
assert_eq!(
|
||||
(data.tegan.person.id, data.bot.person.id, Some(2), Some(0),),
|
||||
(
|
||||
inserted_person_like_2.person_id,
|
||||
inserted_person_like_2.target_id,
|
||||
inserted_person_like_2.upvotes,
|
||||
inserted_person_like_2.downvotes,
|
||||
)
|
||||
);
|
||||
|
||||
// Remove the like
|
||||
let like_removed =
|
||||
PostActions::remove_like(pool, data.tegan.person.id, data.bot_post.id).await?;
|
||||
assert_eq!(uplete::Count::only_deleted(1), like_removed);
|
||||
|
||||
let person_like_removed =
|
||||
PersonActions::remove_like(pool, data.tegan.person.id, data.bot.person.id, 1).await?;
|
||||
assert_eq!(
|
||||
(data.tegan.person.id, data.bot.person.id, Some(1), Some(0),),
|
||||
(
|
||||
person_like_removed.person_id,
|
||||
person_like_removed.target_id,
|
||||
person_like_removed.upvotes,
|
||||
person_like_removed.downvotes,
|
||||
)
|
||||
);
|
||||
|
||||
// Now do a downvote
|
||||
let post_like_form = PostLikeForm::new(data.bot_post.id, data.tegan.person.id, -1);
|
||||
let _inserted_post_dislike = PostActions::like(pool, &post_like_form).await?;
|
||||
let inserted_person_dislike =
|
||||
PersonActions::like(pool, data.tegan.person.id, data.bot.person.id, -1).await?;
|
||||
assert_eq!(
|
||||
(data.tegan.person.id, data.bot.person.id, Some(1), Some(1),),
|
||||
(
|
||||
inserted_person_dislike.person_id,
|
||||
inserted_person_dislike.target_id,
|
||||
inserted_person_dislike.upvotes,
|
||||
inserted_person_dislike.downvotes,
|
||||
)
|
||||
);
|
||||
|
||||
let post_listing = PostView::read(
|
||||
pool,
|
||||
data.bot_post.id,
|
||||
Some(&data.tegan.local_user),
|
||||
data.instance.id,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
(true, true, true, -1, 1, 0),
|
||||
(
|
||||
post_listing
|
||||
.post_actions
|
||||
.is_some_and(|t| t.like_score == Some(-1)),
|
||||
post_listing
|
||||
.person_actions
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.upvotes == Some(1)),
|
||||
post_listing
|
||||
.person_actions
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.downvotes == Some(1)),
|
||||
post_listing.post.score,
|
||||
post_listing.post.downvotes,
|
||||
post_listing.creator.post_score,
|
||||
)
|
||||
);
|
||||
|
||||
let like_removed =
|
||||
PostActions::remove_like(pool, data.tegan.person.id, data.bot_post.id).await?;
|
||||
assert_eq!(uplete::Count::only_deleted(1), like_removed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_context(Data)]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
|
@ -1111,17 +1261,15 @@ mod tests {
|
|||
assert_eq!(expected_post_listing, tegan_listings);
|
||||
|
||||
// Have john become a moderator, then the bot
|
||||
let john_mod_form =
|
||||
CommunityModeratorForm::new(community_id, data.john_local_user_view.person.id);
|
||||
let john_mod_form = CommunityModeratorForm::new(community_id, data.john.person.id);
|
||||
CommunityActions::join(pool, &john_mod_form).await?;
|
||||
|
||||
let bot_mod_form =
|
||||
CommunityModeratorForm::new(community_id, data.bot_local_user_view.person.id);
|
||||
let bot_mod_form = CommunityModeratorForm::new(community_id, data.bot.person.id);
|
||||
CommunityActions::join(pool, &bot_mod_form).await?;
|
||||
|
||||
let john_listings = PostQuery {
|
||||
sort: Some(PostSortType::New),
|
||||
local_user: Some(&data.john_local_user_view.local_user),
|
||||
local_user: Some(&data.john.local_user),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&data.site, pool)
|
||||
|
@ -1142,7 +1290,7 @@ mod tests {
|
|||
// Bot is also a mod, but was added after john, so can't mod anything
|
||||
let bot_listings = PostQuery {
|
||||
sort: Some(PostSortType::New),
|
||||
local_user: Some(&data.bot_local_user_view.local_user),
|
||||
local_user: Some(&data.bot.local_user),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&data.site, pool)
|
||||
|
@ -1164,7 +1312,7 @@ mod tests {
|
|||
|
||||
let bot_listings = PostQuery {
|
||||
sort: Some(PostSortType::New),
|
||||
local_user: Some(&data.bot_local_user_view.local_user),
|
||||
local_user: Some(&data.bot.local_user),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&data.site, pool)
|
||||
|
@ -1187,7 +1335,7 @@ mod tests {
|
|||
|
||||
let john_listings = PostQuery {
|
||||
sort: Some(PostSortType::New),
|
||||
local_user: Some(&data.john_local_user_view.local_user),
|
||||
local_user: Some(&data.john.local_user),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&data.site, pool)
|
||||
|
@ -1331,7 +1479,7 @@ mod tests {
|
|||
// Deleted post is only shown to creator
|
||||
for (local_user, expect_contains_deleted) in [
|
||||
(None, false),
|
||||
(Some(&data.john_local_user_view.local_user), false),
|
||||
(Some(&data.john.local_user), false),
|
||||
(Some(&data.tegan.local_user), true),
|
||||
] {
|
||||
let contains_deleted = PostQuery {
|
||||
|
@ -1415,7 +1563,7 @@ mod tests {
|
|||
language_id: Some(LanguageId(1)),
|
||||
..PostInsertForm::new(
|
||||
POST_FROM_BLOCKED_INSTANCE.to_string(),
|
||||
data.bot_local_user_view.person.id,
|
||||
data.bot.person.id,
|
||||
inserted_community.id,
|
||||
)
|
||||
};
|
||||
|
@ -1848,7 +1996,7 @@ mod tests {
|
|||
let post_view = PostView::read(
|
||||
pool,
|
||||
banned_post.id,
|
||||
Some(&data.john_local_user_view.local_user),
|
||||
Some(&data.john.local_user),
|
||||
data.instance.id,
|
||||
false,
|
||||
)
|
||||
|
|
|
@ -242,7 +242,13 @@ mod tests {
|
|||
last_donation_notification_at: sara_local_user.last_donation_notification_at,
|
||||
show_upvotes: sara_local_user.show_upvotes,
|
||||
show_downvotes: sara_local_user.show_downvotes,
|
||||
..Default::default()
|
||||
admin: sara_local_user.admin,
|
||||
auto_mark_fetched_posts_as_read: sara_local_user.auto_mark_fetched_posts_as_read,
|
||||
hide_media: sara_local_user.hide_media,
|
||||
default_post_time_range_seconds: sara_local_user.default_post_time_range_seconds,
|
||||
show_score: sara_local_user.show_score,
|
||||
show_upvote_percentage: sara_local_user.show_upvote_percentage,
|
||||
show_person_votes: sara_local_user.show_person_votes,
|
||||
},
|
||||
creator: Person {
|
||||
id: sara_person.id,
|
||||
|
|
|
@ -535,6 +535,8 @@ pub struct SaveUserSettings {
|
|||
pub auto_mark_fetched_posts_as_read: Option<bool>,
|
||||
/// Whether to hide posts containing images/videos.
|
||||
pub hide_media: Option<bool>,
|
||||
/// Whether to show vote totals given to others.
|
||||
pub show_person_votes: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||
|
|
8
migrations/2025-06-14-141408_person_votes/down.sql
Normal file
8
migrations/2025-06-14-141408_person_votes/down.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
ALTER TABLE person_actions
|
||||
DROP COLUMN voted_at,
|
||||
DROP COLUMN upvotes,
|
||||
DROP COLUMN downvotes;
|
||||
|
||||
ALTER TABLE local_user
|
||||
DROP COLUMN show_person_votes;
|
||||
|
49
migrations/2025-06-14-141408_person_votes/up.sql
Normal file
49
migrations/2025-06-14-141408_person_votes/up.sql
Normal file
|
@ -0,0 +1,49 @@
|
|||
ALTER TABLE person_actions
|
||||
ADD COLUMN voted_at timestamptz,
|
||||
ADD COLUMN upvotes int,
|
||||
ADD COLUMN downvotes int;
|
||||
|
||||
ALTER TABLE local_user
|
||||
ADD COLUMN show_person_votes boolean NOT NULL DEFAULT TRUE;
|
||||
|
||||
-- Adding vote history
|
||||
-- This union alls the comment and post actions tables,
|
||||
-- inner joins to local_user for the above to filter out non-locals
|
||||
-- separates the like_score into upvote and downvote columns,
|
||||
-- groups and sums the upvotes and downvotes,
|
||||
-- handles conflicts using the `excluded` magic column.
|
||||
INSERT INTO person_actions (person_id, target_id, voted_at, upvotes, downvotes)
|
||||
SELECT
|
||||
votes.person_id,
|
||||
votes.creator_id,
|
||||
now(),
|
||||
count(*) FILTER (WHERE votes.like_score = 1) AS upvotes,
|
||||
count(*) FILTER (WHERE votes.like_score != 1) AS downvotes
|
||||
FROM (
|
||||
SELECT
|
||||
pa.person_id,
|
||||
p.creator_id,
|
||||
like_score
|
||||
FROM
|
||||
post_actions pa
|
||||
INNER JOIN post p ON pa.post_id = p.id
|
||||
AND p.local
|
||||
UNION ALL
|
||||
SELECT
|
||||
ca.person_id,
|
||||
c.creator_id,
|
||||
like_score
|
||||
FROM
|
||||
comment_actions ca
|
||||
INNER JOIN comment c ON ca.comment_id = c.id
|
||||
AND c.local) AS votes
|
||||
GROUP BY
|
||||
votes.person_id,
|
||||
votes.creator_id
|
||||
ON CONFLICT (person_id,
|
||||
target_id)
|
||||
DO UPDATE SET
|
||||
voted_at = now(),
|
||||
upvotes = excluded.upvotes,
|
||||
downvotes = excluded.downvotes;
|
||||
|
Loading…
Reference in a new issue