]> Untitled Git - lemmy.git/blob - crates/db_views/src/post_view.rs
Use same table join code for both read and list functions (#3663)
[lemmy.git] / crates / db_views / src / post_view.rs
1 use crate::structs::{LocalUserView, PostView};
2 use diesel::{
3   debug_query,
4   dsl::{now, IntervalDsl},
5   pg::Pg,
6   result::Error,
7   sql_function,
8   sql_types,
9   BoolExpressionMethods,
10   ExpressionMethods,
11   JoinOnDsl,
12   NullableExpressionMethods,
13   PgTextExpressionMethods,
14   QueryDsl,
15 };
16 use diesel_async::RunQueryDsl;
17 use lemmy_db_schema::{
18   aggregates::structs::PostAggregates,
19   newtypes::{CommunityId, LocalUserId, PersonId, PostId},
20   schema::{
21     community,
22     community_block,
23     community_follower,
24     community_moderator,
25     community_person_ban,
26     local_user_language,
27     person,
28     person_block,
29     person_post_aggregates,
30     post,
31     post_aggregates,
32     post_like,
33     post_read,
34     post_saved,
35   },
36   source::{
37     community::{Community, CommunityFollower, CommunityPersonBan},
38     person::Person,
39     person_block::PersonBlock,
40     post::{Post, PostRead, PostSaved},
41   },
42   traits::JoinView,
43   utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
44   ListingType,
45   SortType,
46 };
47 use tracing::debug;
48
49 type PostViewTuple = (
50   Post,
51   Person,
52   Community,
53   Option<CommunityPersonBan>,
54   PostAggregates,
55   Option<CommunityFollower>,
56   Option<PostSaved>,
57   Option<PostRead>,
58   Option<PersonBlock>,
59   Option<i16>,
60   i64,
61 );
62
63 sql_function!(fn coalesce(x: sql_types::Nullable<sql_types::BigInt>, y: sql_types::BigInt) -> sql_types::BigInt);
64
65 fn queries<'a>() -> Queries<
66   impl ReadFn<'a, PostView, (PostId, Option<PersonId>, Option<bool>)>,
67   impl ListFn<'a, PostView, PostQuery<'a>>,
68 > {
69   let all_joins = |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option<PersonId>| {
70     // The left join below will return None in this case
71     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
72
73     query
74       .inner_join(person::table)
75       .inner_join(community::table)
76       .left_join(
77         community_person_ban::table.on(
78           post_aggregates::community_id
79             .eq(community_person_ban::community_id)
80             .and(community_person_ban::person_id.eq(post_aggregates::creator_id)),
81         ),
82       )
83       .inner_join(post::table)
84       .left_join(
85         community_follower::table.on(
86           post_aggregates::community_id
87             .eq(community_follower::community_id)
88             .and(community_follower::person_id.eq(person_id_join)),
89         ),
90       )
91       .left_join(
92         community_moderator::table.on(
93           post::community_id
94             .eq(community_moderator::community_id)
95             .and(community_moderator::person_id.eq(person_id_join)),
96         ),
97       )
98       .left_join(
99         post_saved::table.on(
100           post_aggregates::post_id
101             .eq(post_saved::post_id)
102             .and(post_saved::person_id.eq(person_id_join)),
103         ),
104       )
105       .left_join(
106         post_read::table.on(
107           post_aggregates::post_id
108             .eq(post_read::post_id)
109             .and(post_read::person_id.eq(person_id_join)),
110         ),
111       )
112       .left_join(
113         person_block::table.on(
114           post_aggregates::creator_id
115             .eq(person_block::target_id)
116             .and(person_block::person_id.eq(person_id_join)),
117         ),
118       )
119       .left_join(
120         post_like::table.on(
121           post_aggregates::post_id
122             .eq(post_like::post_id)
123             .and(post_like::person_id.eq(person_id_join)),
124         ),
125       )
126       .left_join(
127         person_post_aggregates::table.on(
128           post_aggregates::post_id
129             .eq(person_post_aggregates::post_id)
130             .and(person_post_aggregates::person_id.eq(person_id_join)),
131         ),
132       )
133   };
134
135   let selection = (
136     post::all_columns,
137     person::all_columns,
138     community::all_columns,
139     community_person_ban::all_columns.nullable(),
140     post_aggregates::all_columns,
141     community_follower::all_columns.nullable(),
142     post_saved::all_columns.nullable(),
143     post_read::all_columns.nullable(),
144     person_block::all_columns.nullable(),
145     post_like::score.nullable(),
146     coalesce(
147       post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(),
148       post_aggregates::comments,
149     ),
150   );
151
152   let read = move |mut conn: DbConn<'a>,
153                    (post_id, my_person_id, is_mod_or_admin): (
154     PostId,
155     Option<PersonId>,
156     Option<bool>,
157   )| async move {
158     // The left join below will return None in this case
159     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
160
161     let mut query = all_joins(
162       post_aggregates::table
163         .filter(post_aggregates::post_id.eq(post_id))
164         .into_boxed(),
165       my_person_id,
166     )
167     .select(selection);
168
169     // Hide deleted and removed for non-admins or mods
170     if !is_mod_or_admin.unwrap_or(false) {
171       query = query
172         .filter(community::removed.eq(false))
173         .filter(post::removed.eq(false))
174         // users can see their own deleted posts
175         .filter(
176           community::deleted
177             .eq(false)
178             .or(post::creator_id.eq(person_id_join)),
179         )
180         .filter(
181           post::deleted
182             .eq(false)
183             .or(post::creator_id.eq(person_id_join)),
184         );
185     }
186
187     query.first::<PostViewTuple>(&mut conn).await
188   };
189
190   let list = move |mut conn: DbConn<'a>, options: PostQuery<'a>| async move {
191     let person_id = options.local_user.map(|l| l.person.id);
192     let local_user_id = options.local_user.map(|l| l.local_user.id);
193
194     // The left join below will return None in this case
195     let person_id_join = person_id.unwrap_or(PersonId(-1));
196     let local_user_id_join = local_user_id.unwrap_or(LocalUserId(-1));
197
198     let mut query = all_joins(post_aggregates::table.into_boxed(), person_id)
199       .left_join(
200         community_block::table.on(
201           post_aggregates::community_id
202             .eq(community_block::community_id)
203             .and(community_block::person_id.eq(person_id_join)),
204         ),
205       )
206       .left_join(
207         local_user_language::table.on(
208           post::language_id
209             .eq(local_user_language::language_id)
210             .and(local_user_language::local_user_id.eq(local_user_id_join)),
211         ),
212       )
213       .select(selection);
214
215     let is_profile_view = options.is_profile_view.unwrap_or(false);
216     let is_creator = options.creator_id == options.local_user.map(|l| l.person.id);
217     // only show deleted posts to creator
218     if is_creator {
219       query = query
220         .filter(community::deleted.eq(false))
221         .filter(post::deleted.eq(false));
222     }
223
224     let is_admin = options.local_user.map(|l| l.person.admin).unwrap_or(false);
225     // only show removed posts to admin when viewing user profile
226     if !(is_profile_view && is_admin) {
227       query = query
228         .filter(community::removed.eq(false))
229         .filter(post::removed.eq(false));
230     }
231
232     if options.community_id.is_none() {
233       query = query.then_order_by(post_aggregates::featured_local.desc());
234     } else if let Some(community_id) = options.community_id {
235       query = query
236         .filter(post_aggregates::community_id.eq(community_id))
237         .then_order_by(post_aggregates::featured_community.desc());
238     }
239
240     if let Some(creator_id) = options.creator_id {
241       query = query.filter(post_aggregates::creator_id.eq(creator_id));
242     }
243
244     if let Some(listing_type) = options.listing_type {
245       match listing_type {
246         ListingType::Subscribed => {
247           query = query.filter(community_follower::person_id.is_not_null())
248         }
249         ListingType::Local => {
250           query = query.filter(community::local.eq(true)).filter(
251             community::hidden
252               .eq(false)
253               .or(community_follower::person_id.eq(person_id_join)),
254           );
255         }
256         ListingType::All => {
257           query = query.filter(
258             community::hidden
259               .eq(false)
260               .or(community_follower::person_id.eq(person_id_join)),
261           )
262         }
263       }
264     }
265
266     if let Some(url_search) = options.url_search {
267       query = query.filter(post::url.eq(url_search));
268     }
269
270     if let Some(search_term) = options.search_term {
271       let searcher = fuzzy_search(&search_term);
272       query = query.filter(
273         post::name
274           .ilike(searcher.clone())
275           .or(post::body.ilike(searcher)),
276       );
277     }
278
279     if !options
280       .local_user
281       .map(|l| l.local_user.show_nsfw)
282       .unwrap_or(false)
283     {
284       query = query
285         .filter(post::nsfw.eq(false))
286         .filter(community::nsfw.eq(false));
287     };
288
289     if !options
290       .local_user
291       .map(|l| l.local_user.show_bot_accounts)
292       .unwrap_or(true)
293     {
294       query = query.filter(person::bot_account.eq(false));
295     };
296
297     if options.saved_only.unwrap_or(false) {
298       query = query.filter(post_saved::post_id.is_not_null());
299     }
300
301     if options.moderator_view.unwrap_or(false) {
302       query = query.filter(community_moderator::person_id.is_not_null());
303     }
304     // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read
305     // setting wont be able to see saved posts.
306     else if !options
307       .local_user
308       .map(|l| l.local_user.show_read_posts)
309       .unwrap_or(true)
310     {
311       query = query.filter(post_read::post_id.is_null());
312     }
313
314     if options.local_user.is_some() {
315       // Filter out the rows with missing languages
316       query = query.filter(local_user_language::language_id.is_not_null());
317
318       // Don't show blocked communities or persons
319       query = query.filter(community_block::person_id.is_null());
320       if !options.moderator_view.unwrap_or(false) {
321         query = query.filter(person_block::person_id.is_null());
322       }
323     }
324
325     query = match options.sort.unwrap_or(SortType::Hot) {
326       SortType::Active => query
327         .then_order_by(post_aggregates::hot_rank_active.desc())
328         .then_order_by(post_aggregates::published.desc()),
329       SortType::Hot => query
330         .then_order_by(post_aggregates::hot_rank.desc())
331         .then_order_by(post_aggregates::published.desc()),
332       SortType::Controversial => query.then_order_by(post_aggregates::controversy_rank.desc()),
333       SortType::New => query.then_order_by(post_aggregates::published.desc()),
334       SortType::Old => query.then_order_by(post_aggregates::published.asc()),
335       SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
336       SortType::MostComments => query
337         .then_order_by(post_aggregates::comments.desc())
338         .then_order_by(post_aggregates::published.desc()),
339       SortType::TopAll => query
340         .then_order_by(post_aggregates::score.desc())
341         .then_order_by(post_aggregates::published.desc()),
342       SortType::TopYear => query
343         .filter(post_aggregates::published.gt(now - 1.years()))
344         .then_order_by(post_aggregates::score.desc())
345         .then_order_by(post_aggregates::published.desc()),
346       SortType::TopMonth => query
347         .filter(post_aggregates::published.gt(now - 1.months()))
348         .then_order_by(post_aggregates::score.desc())
349         .then_order_by(post_aggregates::published.desc()),
350       SortType::TopWeek => query
351         .filter(post_aggregates::published.gt(now - 1.weeks()))
352         .then_order_by(post_aggregates::score.desc())
353         .then_order_by(post_aggregates::published.desc()),
354       SortType::TopDay => query
355         .filter(post_aggregates::published.gt(now - 1.days()))
356         .then_order_by(post_aggregates::score.desc())
357         .then_order_by(post_aggregates::published.desc()),
358       SortType::TopHour => query
359         .filter(post_aggregates::published.gt(now - 1.hours()))
360         .then_order_by(post_aggregates::score.desc())
361         .then_order_by(post_aggregates::published.desc()),
362       SortType::TopSixHour => query
363         .filter(post_aggregates::published.gt(now - 6.hours()))
364         .then_order_by(post_aggregates::score.desc())
365         .then_order_by(post_aggregates::published.desc()),
366       SortType::TopTwelveHour => query
367         .filter(post_aggregates::published.gt(now - 12.hours()))
368         .then_order_by(post_aggregates::score.desc())
369         .then_order_by(post_aggregates::published.desc()),
370       SortType::TopThreeMonths => query
371         .filter(post_aggregates::published.gt(now - 3.months()))
372         .then_order_by(post_aggregates::score.desc())
373         .then_order_by(post_aggregates::published.desc()),
374       SortType::TopSixMonths => query
375         .filter(post_aggregates::published.gt(now - 6.months()))
376         .then_order_by(post_aggregates::score.desc())
377         .then_order_by(post_aggregates::published.desc()),
378       SortType::TopNineMonths => query
379         .filter(post_aggregates::published.gt(now - 9.months()))
380         .then_order_by(post_aggregates::score.desc())
381         .then_order_by(post_aggregates::published.desc()),
382     };
383
384     let (limit, offset) = limit_and_offset(options.page, options.limit)?;
385
386     query = query.limit(limit).offset(offset);
387
388     debug!("Post View Query: {:?}", debug_query::<Pg, _>(&query));
389
390     query.load::<PostViewTuple>(&mut conn).await
391   };
392
393   Queries::new(read, list)
394 }
395
396 impl PostView {
397   pub async fn read(
398     pool: &mut DbPool<'_>,
399     post_id: PostId,
400     my_person_id: Option<PersonId>,
401     is_mod_or_admin: Option<bool>,
402   ) -> Result<Self, Error> {
403     let mut res = queries()
404       .read(pool, (post_id, my_person_id, is_mod_or_admin))
405       .await?;
406
407     // If a person is given, then my_vote, if None, should be 0, not null
408     // Necessary to differentiate between other person's votes
409     if my_person_id.is_some() && res.my_vote.is_none() {
410       res.my_vote = Some(0)
411     };
412
413     Ok(res)
414   }
415 }
416
417 #[derive(Default)]
418 pub struct PostQuery<'a> {
419   pub listing_type: Option<ListingType>,
420   pub sort: Option<SortType>,
421   pub creator_id: Option<PersonId>,
422   pub community_id: Option<CommunityId>,
423   pub local_user: Option<&'a LocalUserView>,
424   pub search_term: Option<String>,
425   pub url_search: Option<String>,
426   pub saved_only: Option<bool>,
427   pub moderator_view: Option<bool>,
428   pub is_profile_view: Option<bool>,
429   pub page: Option<i64>,
430   pub limit: Option<i64>,
431 }
432
433 impl<'a> PostQuery<'a> {
434   pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<PostView>, Error> {
435     queries().list(pool, self).await
436   }
437 }
438
439 impl JoinView for PostView {
440   type JoinTuple = PostViewTuple;
441   fn from_tuple(a: Self::JoinTuple) -> Self {
442     Self {
443       post: a.0,
444       creator: a.1,
445       community: a.2,
446       creator_banned_from_community: a.3.is_some(),
447       counts: a.4,
448       subscribed: CommunityFollower::to_subscribed_type(&a.5),
449       saved: a.6.is_some(),
450       read: a.7.is_some(),
451       creator_blocked: a.8.is_some(),
452       my_vote: a.9,
453       unread_comments: a.10,
454     }
455   }
456 }
457
458 #[cfg(test)]
459 mod tests {
460   #![allow(clippy::unwrap_used)]
461   #![allow(clippy::indexing_slicing)]
462
463   use crate::{
464     post_view::{PostQuery, PostView},
465     structs::LocalUserView,
466   };
467   use lemmy_db_schema::{
468     aggregates::structs::PostAggregates,
469     impls::actor_language::UNDETERMINED_ID,
470     newtypes::LanguageId,
471     source::{
472       actor_language::LocalUserLanguage,
473       community::{Community, CommunityInsertForm},
474       community_block::{CommunityBlock, CommunityBlockForm},
475       instance::Instance,
476       language::Language,
477       local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
478       person::{Person, PersonInsertForm},
479       person_block::{PersonBlock, PersonBlockForm},
480       post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
481     },
482     traits::{Blockable, Crud, Likeable},
483     utils::{build_db_pool_for_tests, DbPool},
484     SortType,
485     SubscribedType,
486   };
487   use serial_test::serial;
488
489   struct Data {
490     inserted_instance: Instance,
491     local_user_view: LocalUserView,
492     inserted_blocked_person: Person,
493     inserted_bot: Person,
494     inserted_community: Community,
495     inserted_post: Post,
496   }
497
498   async fn init_data(pool: &mut DbPool<'_>) -> Data {
499     let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
500       .await
501       .unwrap();
502
503     let person_name = "tegan".to_string();
504
505     let new_person = PersonInsertForm::builder()
506       .name(person_name.clone())
507       .public_key("pubkey".to_string())
508       .instance_id(inserted_instance.id)
509       .build();
510
511     let inserted_person = Person::create(pool, &new_person).await.unwrap();
512
513     let local_user_form = LocalUserInsertForm::builder()
514       .person_id(inserted_person.id)
515       .password_encrypted(String::new())
516       .build();
517     let inserted_local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
518
519     let new_bot = PersonInsertForm::builder()
520       .name("mybot".to_string())
521       .bot_account(Some(true))
522       .public_key("pubkey".to_string())
523       .instance_id(inserted_instance.id)
524       .build();
525
526     let inserted_bot = Person::create(pool, &new_bot).await.unwrap();
527
528     let new_community = CommunityInsertForm::builder()
529       .name("test_community_3".to_string())
530       .title("nada".to_owned())
531       .public_key("pubkey".to_string())
532       .instance_id(inserted_instance.id)
533       .build();
534
535     let inserted_community = Community::create(pool, &new_community).await.unwrap();
536
537     // Test a person block, make sure the post query doesn't include their post
538     let blocked_person = PersonInsertForm::builder()
539       .name(person_name)
540       .public_key("pubkey".to_string())
541       .instance_id(inserted_instance.id)
542       .build();
543
544     let inserted_blocked_person = Person::create(pool, &blocked_person).await.unwrap();
545
546     let post_from_blocked_person = PostInsertForm::builder()
547       .name("blocked_person_post".to_string())
548       .creator_id(inserted_blocked_person.id)
549       .community_id(inserted_community.id)
550       .language_id(Some(LanguageId(1)))
551       .build();
552
553     Post::create(pool, &post_from_blocked_person).await.unwrap();
554
555     // block that person
556     let person_block = PersonBlockForm {
557       person_id: inserted_person.id,
558       target_id: inserted_blocked_person.id,
559     };
560
561     PersonBlock::block(pool, &person_block).await.unwrap();
562
563     // A sample post
564     let new_post = PostInsertForm::builder()
565       .name("test post 3".to_string())
566       .creator_id(inserted_person.id)
567       .community_id(inserted_community.id)
568       .language_id(Some(LanguageId(47)))
569       .build();
570
571     let inserted_post = Post::create(pool, &new_post).await.unwrap();
572
573     let new_bot_post = PostInsertForm::builder()
574       .name("test bot post".to_string())
575       .creator_id(inserted_bot.id)
576       .community_id(inserted_community.id)
577       .build();
578
579     let _inserted_bot_post = Post::create(pool, &new_bot_post).await.unwrap();
580     let local_user_view = LocalUserView {
581       local_user: inserted_local_user,
582       person: inserted_person,
583       counts: Default::default(),
584     };
585
586     Data {
587       inserted_instance,
588       local_user_view,
589       inserted_blocked_person,
590       inserted_bot,
591       inserted_community,
592       inserted_post,
593     }
594   }
595
596   #[tokio::test]
597   #[serial]
598   async fn post_listing_with_person() {
599     let pool = &build_db_pool_for_tests().await;
600     let pool = &mut pool.into();
601     let mut data = init_data(pool).await;
602
603     let local_user_form = LocalUserUpdateForm::builder()
604       .show_bot_accounts(Some(false))
605       .build();
606     let inserted_local_user =
607       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
608         .await
609         .unwrap();
610     data.local_user_view.local_user = inserted_local_user;
611
612     let read_post_listing = PostQuery {
613       sort: (Some(SortType::New)),
614       community_id: (Some(data.inserted_community.id)),
615       local_user: (Some(&data.local_user_view)),
616       ..Default::default()
617     }
618     .list(pool)
619     .await
620     .unwrap();
621
622     let post_listing_single_with_person = PostView::read(
623       pool,
624       data.inserted_post.id,
625       Some(data.local_user_view.person.id),
626       None,
627     )
628     .await
629     .unwrap();
630
631     let mut expected_post_listing_with_user = expected_post_view(&data, pool).await;
632
633     // Should be only one person, IE the bot post, and blocked should be missing
634     assert_eq!(1, read_post_listing.len());
635
636     assert_eq!(expected_post_listing_with_user, read_post_listing[0]);
637     expected_post_listing_with_user.my_vote = Some(0);
638     assert_eq!(
639       expected_post_listing_with_user,
640       post_listing_single_with_person
641     );
642
643     let local_user_form = LocalUserUpdateForm::builder()
644       .show_bot_accounts(Some(true))
645       .build();
646     let inserted_local_user =
647       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
648         .await
649         .unwrap();
650     data.local_user_view.local_user = inserted_local_user;
651
652     let post_listings_with_bots = PostQuery {
653       sort: (Some(SortType::New)),
654       community_id: (Some(data.inserted_community.id)),
655       local_user: (Some(&data.local_user_view)),
656       ..Default::default()
657     }
658     .list(pool)
659     .await
660     .unwrap();
661     // should include bot post which has "undetermined" language
662     assert_eq!(2, post_listings_with_bots.len());
663
664     cleanup(data, pool).await;
665   }
666
667   #[tokio::test]
668   #[serial]
669   async fn post_listing_no_person() {
670     let pool = &build_db_pool_for_tests().await;
671     let pool = &mut pool.into();
672     let data = init_data(pool).await;
673
674     let read_post_listing_multiple_no_person = PostQuery {
675       sort: (Some(SortType::New)),
676       community_id: (Some(data.inserted_community.id)),
677       ..Default::default()
678     }
679     .list(pool)
680     .await
681     .unwrap();
682
683     let read_post_listing_single_no_person =
684       PostView::read(pool, data.inserted_post.id, None, None)
685         .await
686         .unwrap();
687
688     let expected_post_listing_no_person = expected_post_view(&data, pool).await;
689
690     // Should be 2 posts, with the bot post, and the blocked
691     assert_eq!(3, read_post_listing_multiple_no_person.len());
692
693     assert_eq!(
694       expected_post_listing_no_person,
695       read_post_listing_multiple_no_person[1]
696     );
697     assert_eq!(
698       expected_post_listing_no_person,
699       read_post_listing_single_no_person
700     );
701
702     cleanup(data, pool).await;
703   }
704
705   #[tokio::test]
706   #[serial]
707   async fn post_listing_block_community() {
708     let pool = &build_db_pool_for_tests().await;
709     let pool = &mut pool.into();
710     let data = init_data(pool).await;
711
712     let community_block = CommunityBlockForm {
713       person_id: data.local_user_view.person.id,
714       community_id: data.inserted_community.id,
715     };
716     CommunityBlock::block(pool, &community_block).await.unwrap();
717
718     let read_post_listings_with_person_after_block = PostQuery {
719       sort: (Some(SortType::New)),
720       community_id: (Some(data.inserted_community.id)),
721       local_user: (Some(&data.local_user_view)),
722       ..Default::default()
723     }
724     .list(pool)
725     .await
726     .unwrap();
727     // Should be 0 posts after the community block
728     assert_eq!(0, read_post_listings_with_person_after_block.len());
729
730     CommunityBlock::unblock(pool, &community_block)
731       .await
732       .unwrap();
733     cleanup(data, pool).await;
734   }
735
736   #[tokio::test]
737   #[serial]
738   async fn post_listing_like() {
739     let pool = &build_db_pool_for_tests().await;
740     let pool = &mut pool.into();
741     let mut data = init_data(pool).await;
742
743     let post_like_form = PostLikeForm {
744       post_id: data.inserted_post.id,
745       person_id: data.local_user_view.person.id,
746       score: 1,
747     };
748
749     let inserted_post_like = PostLike::like(pool, &post_like_form).await.unwrap();
750
751     let expected_post_like = PostLike {
752       id: inserted_post_like.id,
753       post_id: data.inserted_post.id,
754       person_id: data.local_user_view.person.id,
755       published: inserted_post_like.published,
756       score: 1,
757     };
758     assert_eq!(expected_post_like, inserted_post_like);
759
760     let post_listing_single_with_person = PostView::read(
761       pool,
762       data.inserted_post.id,
763       Some(data.local_user_view.person.id),
764       None,
765     )
766     .await
767     .unwrap();
768
769     let mut expected_post_with_upvote = expected_post_view(&data, pool).await;
770     expected_post_with_upvote.my_vote = Some(1);
771     expected_post_with_upvote.counts.score = 1;
772     expected_post_with_upvote.counts.upvotes = 1;
773     assert_eq!(expected_post_with_upvote, post_listing_single_with_person);
774
775     let local_user_form = LocalUserUpdateForm::builder()
776       .show_bot_accounts(Some(false))
777       .build();
778     let inserted_local_user =
779       LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
780         .await
781         .unwrap();
782     data.local_user_view.local_user = inserted_local_user;
783
784     let read_post_listing = PostQuery {
785       sort: (Some(SortType::New)),
786       community_id: (Some(data.inserted_community.id)),
787       local_user: (Some(&data.local_user_view)),
788       ..Default::default()
789     }
790     .list(pool)
791     .await
792     .unwrap();
793     assert_eq!(1, read_post_listing.len());
794
795     assert_eq!(expected_post_with_upvote, read_post_listing[0]);
796
797     let like_removed =
798       PostLike::remove(pool, data.local_user_view.person.id, data.inserted_post.id)
799         .await
800         .unwrap();
801     assert_eq!(1, like_removed);
802     cleanup(data, pool).await;
803   }
804
805   #[tokio::test]
806   #[serial]
807   async fn post_listing_person_language() {
808     let pool = &build_db_pool_for_tests().await;
809     let pool = &mut pool.into();
810     let data = init_data(pool).await;
811
812     let spanish_id = Language::read_id_from_code(pool, Some("es"))
813       .await
814       .unwrap()
815       .unwrap();
816     let post_spanish = PostInsertForm::builder()
817       .name("asffgdsc".to_string())
818       .creator_id(data.local_user_view.person.id)
819       .community_id(data.inserted_community.id)
820       .language_id(Some(spanish_id))
821       .build();
822
823     Post::create(pool, &post_spanish).await.unwrap();
824
825     let post_listings_all = PostQuery {
826       sort: (Some(SortType::New)),
827       local_user: (Some(&data.local_user_view)),
828       ..Default::default()
829     }
830     .list(pool)
831     .await
832     .unwrap();
833
834     // no language filters specified, all posts should be returned
835     assert_eq!(3, post_listings_all.len());
836
837     let french_id = Language::read_id_from_code(pool, Some("fr"))
838       .await
839       .unwrap()
840       .unwrap();
841     LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id)
842       .await
843       .unwrap();
844
845     let post_listing_french = PostQuery {
846       sort: (Some(SortType::New)),
847       local_user: (Some(&data.local_user_view)),
848       ..Default::default()
849     }
850     .list(pool)
851     .await
852     .unwrap();
853
854     // only one post in french and one undetermined should be returned
855     assert_eq!(2, post_listing_french.len());
856     assert!(post_listing_french
857       .iter()
858       .any(|p| p.post.language_id == french_id));
859
860     LocalUserLanguage::update(
861       pool,
862       vec![french_id, UNDETERMINED_ID],
863       data.local_user_view.local_user.id,
864     )
865     .await
866     .unwrap();
867     let post_listings_french_und = PostQuery {
868       sort: (Some(SortType::New)),
869       local_user: (Some(&data.local_user_view)),
870       ..Default::default()
871     }
872     .list(pool)
873     .await
874     .unwrap();
875
876     // french post and undetermined language post should be returned
877     assert_eq!(2, post_listings_french_und.len());
878     assert_eq!(
879       UNDETERMINED_ID,
880       post_listings_french_und[0].post.language_id
881     );
882     assert_eq!(french_id, post_listings_french_und[1].post.language_id);
883
884     cleanup(data, pool).await;
885   }
886
887   #[tokio::test]
888   #[serial]
889   async fn post_listings_removed() {
890     let pool = &build_db_pool_for_tests().await;
891     let pool = &mut pool.into();
892     let mut data = init_data(pool).await;
893
894     // Remove the post
895     Post::update(
896       pool,
897       data.inserted_post.id,
898       &PostUpdateForm::builder().removed(Some(true)).build(),
899     )
900     .await
901     .unwrap();
902
903     // Make sure you don't see the removed post in the results
904     let post_listings_no_admin = PostQuery {
905       sort: Some(SortType::New),
906       local_user: Some(&data.local_user_view),
907       ..Default::default()
908     }
909     .list(pool)
910     .await
911     .unwrap();
912     assert_eq!(1, post_listings_no_admin.len());
913
914     // Removed post is shown to admins on profile page
915     data.local_user_view.person.admin = true;
916     let post_listings_is_admin = PostQuery {
917       sort: Some(SortType::New),
918       local_user: Some(&data.local_user_view),
919       is_profile_view: Some(true),
920       ..Default::default()
921     }
922     .list(pool)
923     .await
924     .unwrap();
925     assert_eq!(2, post_listings_is_admin.len());
926
927     cleanup(data, pool).await;
928   }
929
930   #[tokio::test]
931   #[serial]
932   async fn post_listings_deleted() {
933     let pool = &build_db_pool_for_tests().await;
934     let pool = &mut pool.into();
935     let data = init_data(pool).await;
936
937     // Delete the post
938     Post::update(
939       pool,
940       data.inserted_post.id,
941       &PostUpdateForm::builder().deleted(Some(true)).build(),
942     )
943     .await
944     .unwrap();
945
946     // Make sure you don't see the deleted post in the results
947     let post_listings_no_creator = PostQuery {
948       sort: Some(SortType::New),
949       ..Default::default()
950     }
951     .list(pool)
952     .await
953     .unwrap();
954     let not_contains_deleted = post_listings_no_creator
955       .iter()
956       .map(|p| p.post.id)
957       .all(|p| p != data.inserted_post.id);
958     assert!(not_contains_deleted);
959
960     // Deleted post is shown to creator
961     let post_listings_is_creator = PostQuery {
962       sort: Some(SortType::New),
963       local_user: Some(&data.local_user_view),
964       ..Default::default()
965     }
966     .list(pool)
967     .await
968     .unwrap();
969     let contains_deleted = post_listings_is_creator
970       .iter()
971       .map(|p| p.post.id)
972       .any(|p| p == data.inserted_post.id);
973     assert!(contains_deleted);
974
975     cleanup(data, pool).await;
976   }
977
978   async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
979     let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap();
980     Community::delete(pool, data.inserted_community.id)
981       .await
982       .unwrap();
983     Person::delete(pool, data.local_user_view.person.id)
984       .await
985       .unwrap();
986     Person::delete(pool, data.inserted_bot.id).await.unwrap();
987     Person::delete(pool, data.inserted_blocked_person.id)
988       .await
989       .unwrap();
990     Instance::delete(pool, data.inserted_instance.id)
991       .await
992       .unwrap();
993     assert_eq!(1, num_deleted);
994   }
995
996   async fn expected_post_view(data: &Data, pool: &mut DbPool<'_>) -> PostView {
997     let (inserted_person, inserted_community, inserted_post) = (
998       &data.local_user_view.person,
999       &data.inserted_community,
1000       &data.inserted_post,
1001     );
1002     let agg = PostAggregates::read(pool, inserted_post.id).await.unwrap();
1003
1004     PostView {
1005       post: Post {
1006         id: inserted_post.id,
1007         name: inserted_post.name.clone(),
1008         creator_id: inserted_person.id,
1009         url: None,
1010         body: None,
1011         published: inserted_post.published,
1012         updated: None,
1013         community_id: inserted_community.id,
1014         removed: false,
1015         deleted: false,
1016         locked: false,
1017         nsfw: false,
1018         embed_title: None,
1019         embed_description: None,
1020         embed_video_url: None,
1021         thumbnail_url: None,
1022         ap_id: inserted_post.ap_id.clone(),
1023         local: true,
1024         language_id: LanguageId(47),
1025         featured_community: false,
1026         featured_local: false,
1027       },
1028       my_vote: None,
1029       unread_comments: 0,
1030       creator: Person {
1031         id: inserted_person.id,
1032         name: inserted_person.name.clone(),
1033         display_name: None,
1034         published: inserted_person.published,
1035         avatar: None,
1036         actor_id: inserted_person.actor_id.clone(),
1037         local: true,
1038         admin: false,
1039         bot_account: false,
1040         banned: false,
1041         deleted: false,
1042         bio: None,
1043         banner: None,
1044         updated: None,
1045         inbox_url: inserted_person.inbox_url.clone(),
1046         shared_inbox_url: None,
1047         matrix_user_id: None,
1048         ban_expires: None,
1049         instance_id: data.inserted_instance.id,
1050         private_key: inserted_person.private_key.clone(),
1051         public_key: inserted_person.public_key.clone(),
1052         last_refreshed_at: inserted_person.last_refreshed_at,
1053       },
1054       creator_banned_from_community: false,
1055       community: Community {
1056         id: inserted_community.id,
1057         name: inserted_community.name.clone(),
1058         icon: None,
1059         removed: false,
1060         deleted: false,
1061         nsfw: false,
1062         actor_id: inserted_community.actor_id.clone(),
1063         local: true,
1064         title: "nada".to_owned(),
1065         description: None,
1066         updated: None,
1067         banner: None,
1068         hidden: false,
1069         posting_restricted_to_mods: false,
1070         published: inserted_community.published,
1071         instance_id: data.inserted_instance.id,
1072         private_key: inserted_community.private_key.clone(),
1073         public_key: inserted_community.public_key.clone(),
1074         last_refreshed_at: inserted_community.last_refreshed_at,
1075         followers_url: inserted_community.followers_url.clone(),
1076         inbox_url: inserted_community.inbox_url.clone(),
1077         shared_inbox_url: inserted_community.shared_inbox_url.clone(),
1078         moderators_url: inserted_community.moderators_url.clone(),
1079         featured_url: inserted_community.featured_url.clone(),
1080       },
1081       counts: PostAggregates {
1082         id: agg.id,
1083         post_id: inserted_post.id,
1084         comments: 0,
1085         score: 0,
1086         upvotes: 0,
1087         downvotes: 0,
1088         published: agg.published,
1089         newest_comment_time_necro: inserted_post.published,
1090         newest_comment_time: inserted_post.published,
1091         featured_community: false,
1092         featured_local: false,
1093         hot_rank: 1728,
1094         hot_rank_active: 1728,
1095         controversy_rank: 0.0,
1096         community_id: inserted_post.community_id,
1097         creator_id: inserted_post.creator_id,
1098       },
1099       subscribed: SubscribedType::NotSubscribed,
1100       read: false,
1101       saved: false,
1102       creator_blocked: false,
1103     }
1104   }
1105 }