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