]> Untitled Git - lemmy.git/blob - lemmy_db/src/views/post_view.rs
Merge remote-tracking branch 'origin/split-db-workspace' into move_views_to_diesel_split
[lemmy.git] / lemmy_db / src / views / post_view.rs
1 use crate::{
2   aggregates::post_aggregates::PostAggregates,
3   functions::hot_rank,
4   fuzzy_search,
5   limit_and_offset,
6   views::ViewToVec,
7   ListingType,
8   MaybeOptional,
9   SortType,
10   ToSafe,
11 };
12 use diesel::{result::Error, *};
13 use lemmy_db_schema::{
14   schema::{
15     community,
16     community_follower,
17     community_user_ban,
18     post,
19     post_aggregates,
20     post_like,
21     post_read,
22     post_saved,
23     user_,
24   },
25   source::{
26     community::{Community, CommunityFollower, CommunitySafe, CommunityUserBan},
27     post::{Post, PostRead, PostSaved},
28     user::{UserSafe, User_},
29   },
30 };
31 use serde::Serialize;
32
33 #[derive(Debug, PartialEq, Serialize, Clone)]
34 pub struct PostView {
35   pub post: Post,
36   pub creator: UserSafe,
37   pub community: CommunitySafe,
38   pub creator_banned_from_community: bool, // Left Join to CommunityUserBan
39   pub counts: PostAggregates,
40   pub subscribed: bool,     // Left join to CommunityFollower
41   pub saved: bool,          // Left join to PostSaved
42   pub read: bool,           // Left join to PostRead
43   pub my_vote: Option<i16>, // Left join to PostLike
44 }
45
46 type PostViewTuple = (
47   Post,
48   UserSafe,
49   CommunitySafe,
50   Option<CommunityUserBan>,
51   PostAggregates,
52   Option<CommunityFollower>,
53   Option<PostSaved>,
54   Option<PostRead>,
55   Option<i16>,
56 );
57
58 impl PostView {
59   pub fn read(conn: &PgConnection, post_id: i32, my_user_id: Option<i32>) -> Result<Self, Error> {
60     // The left join below will return None in this case
61     let user_id_join = my_user_id.unwrap_or(-1);
62
63     let (
64       post,
65       creator,
66       community,
67       creator_banned_from_community,
68       counts,
69       follower,
70       saved,
71       read,
72       my_vote,
73     ) = post::table
74       .find(post_id)
75       .inner_join(user_::table)
76       .inner_join(community::table)
77       .left_join(
78         community_user_ban::table.on(
79           post::community_id
80             .eq(community_user_ban::community_id)
81             .and(community_user_ban::user_id.eq(post::creator_id)),
82         ),
83       )
84       .inner_join(post_aggregates::table)
85       .left_join(
86         community_follower::table.on(
87           post::community_id
88             .eq(community_follower::community_id)
89             .and(community_follower::user_id.eq(user_id_join)),
90         ),
91       )
92       .left_join(
93         post_saved::table.on(
94           post::id
95             .eq(post_saved::post_id)
96             .and(post_saved::user_id.eq(user_id_join)),
97         ),
98       )
99       .left_join(
100         post_read::table.on(
101           post::id
102             .eq(post_read::post_id)
103             .and(post_read::user_id.eq(user_id_join)),
104         ),
105       )
106       .left_join(
107         post_like::table.on(
108           post::id
109             .eq(post_like::post_id)
110             .and(post_like::user_id.eq(user_id_join)),
111         ),
112       )
113       .select((
114         post::all_columns,
115         User_::safe_columns_tuple(),
116         Community::safe_columns_tuple(),
117         community_user_ban::all_columns.nullable(),
118         post_aggregates::all_columns,
119         community_follower::all_columns.nullable(),
120         post_saved::all_columns.nullable(),
121         post_read::all_columns.nullable(),
122         post_like::score.nullable(),
123       ))
124       .first::<PostViewTuple>(conn)?;
125
126     Ok(PostView {
127       post,
128       creator,
129       community,
130       creator_banned_from_community: creator_banned_from_community.is_some(),
131       counts,
132       subscribed: follower.is_some(),
133       saved: saved.is_some(),
134       read: read.is_some(),
135       my_vote,
136     })
137   }
138 }
139
140 pub struct PostQueryBuilder<'a> {
141   conn: &'a PgConnection,
142   listing_type: &'a ListingType,
143   sort: &'a SortType,
144   creator_id: Option<i32>,
145   community_id: Option<i32>,
146   community_name: Option<String>,
147   my_user_id: Option<i32>,
148   search_term: Option<String>,
149   url_search: Option<String>,
150   show_nsfw: bool,
151   saved_only: bool,
152   unread_only: bool,
153   page: Option<i64>,
154   limit: Option<i64>,
155 }
156
157 impl<'a> PostQueryBuilder<'a> {
158   pub fn create(conn: &'a PgConnection) -> Self {
159     PostQueryBuilder {
160       conn,
161       listing_type: &ListingType::All,
162       sort: &SortType::Hot,
163       creator_id: None,
164       community_id: None,
165       community_name: None,
166       my_user_id: None,
167       search_term: None,
168       url_search: None,
169       show_nsfw: true,
170       saved_only: false,
171       unread_only: false,
172       page: None,
173       limit: None,
174     }
175   }
176
177   pub fn listing_type(mut self, listing_type: &'a ListingType) -> Self {
178     self.listing_type = listing_type;
179     self
180   }
181
182   pub fn sort(mut self, sort: &'a SortType) -> Self {
183     self.sort = sort;
184     self
185   }
186
187   pub fn community_id<T: MaybeOptional<i32>>(mut self, community_id: T) -> Self {
188     self.community_id = community_id.get_optional();
189     self
190   }
191
192   pub fn my_user_id<T: MaybeOptional<i32>>(mut self, my_user_id: T) -> Self {
193     self.my_user_id = my_user_id.get_optional();
194     self
195   }
196
197   pub fn community_name<T: MaybeOptional<String>>(mut self, community_name: T) -> Self {
198     self.community_name = community_name.get_optional();
199     self
200   }
201
202   pub fn creator_id<T: MaybeOptional<i32>>(mut self, creator_id: T) -> Self {
203     self.creator_id = creator_id.get_optional();
204     self
205   }
206
207   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
208     self.search_term = search_term.get_optional();
209     self
210   }
211
212   pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
213     self.url_search = url_search.get_optional();
214     self
215   }
216
217   pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
218     self.show_nsfw = show_nsfw;
219     self
220   }
221
222   pub fn saved_only(mut self, saved_only: bool) -> Self {
223     self.saved_only = saved_only;
224     self
225   }
226
227   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
228     self.page = page.get_optional();
229     self
230   }
231
232   pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
233     self.limit = limit.get_optional();
234     self
235   }
236
237   pub fn list(self) -> Result<Vec<PostView>, Error> {
238     use diesel::dsl::*;
239
240     // The left join below will return None in this case
241     let user_id_join = self.my_user_id.unwrap_or(-1);
242
243     let mut query = post::table
244       .inner_join(user_::table)
245       .inner_join(community::table)
246       .left_join(
247         community_user_ban::table.on(
248           post::community_id
249             .eq(community_user_ban::community_id)
250             .and(community_user_ban::user_id.eq(community::creator_id)),
251         ),
252       )
253       .inner_join(post_aggregates::table)
254       .left_join(
255         community_follower::table.on(
256           post::community_id
257             .eq(community_follower::community_id)
258             .and(community_follower::user_id.eq(user_id_join)),
259         ),
260       )
261       .left_join(
262         post_saved::table.on(
263           post::id
264             .eq(post_saved::post_id)
265             .and(post_saved::user_id.eq(user_id_join)),
266         ),
267       )
268       .left_join(
269         post_read::table.on(
270           post::id
271             .eq(post_read::post_id)
272             .and(post_read::user_id.eq(user_id_join)),
273         ),
274       )
275       .left_join(
276         post_like::table.on(
277           post::id
278             .eq(post_like::post_id)
279             .and(post_like::user_id.eq(user_id_join)),
280         ),
281       )
282       .select((
283         post::all_columns,
284         User_::safe_columns_tuple(),
285         Community::safe_columns_tuple(),
286         community_user_ban::all_columns.nullable(),
287         post_aggregates::all_columns,
288         community_follower::all_columns.nullable(),
289         post_saved::all_columns.nullable(),
290         post_read::all_columns.nullable(),
291         post_like::score.nullable(),
292       ))
293       .into_boxed();
294
295     query = match self.listing_type {
296       ListingType::Subscribed => query.filter(community_follower::user_id.is_not_null()), // TODO could be this: and(community_follower::user_id.eq(user_id_join)),
297       ListingType::Local => query.filter(community::local.eq(true)),
298       _ => query,
299     };
300
301     if let Some(community_id) = self.community_id {
302       query = query
303         .filter(post::community_id.eq(community_id))
304         .then_order_by(post::stickied.desc());
305     }
306
307     if let Some(community_name) = self.community_name {
308       query = query
309         .filter(community::name.eq(community_name))
310         .filter(community::local.eq(true))
311         .then_order_by(post::stickied.desc());
312     }
313
314     if let Some(url_search) = self.url_search {
315       query = query.filter(post::url.eq(url_search));
316     }
317
318     if let Some(search_term) = self.search_term {
319       let searcher = fuzzy_search(&search_term);
320       query = query.filter(
321         post::name
322           .ilike(searcher.to_owned())
323           .or(post::body.ilike(searcher)),
324       );
325     }
326
327     // If its for a specific user, show the removed / deleted
328     if let Some(creator_id) = self.creator_id {
329       query = query.filter(post::creator_id.eq(creator_id));
330     }
331
332     if !self.show_nsfw {
333       query = query
334         .filter(post::nsfw.eq(false))
335         .filter(community::nsfw.eq(false));
336     };
337
338     // TODO  These two might be wrong
339     if self.saved_only {
340       query = query.filter(post_saved::id.is_not_null());
341     };
342
343     if self.unread_only {
344       query = query.filter(post_read::id.is_not_null());
345     };
346
347     query = match self.sort {
348       SortType::Active => query
349         .then_order_by(
350           hot_rank(post_aggregates::score, post_aggregates::newest_comment_time).desc(),
351         )
352         .then_order_by(post::published.desc()),
353       SortType::Hot => query
354         .then_order_by(hot_rank(post_aggregates::score, post::published).desc())
355         .then_order_by(post::published.desc()),
356       SortType::New => query.then_order_by(post::published.desc()),
357       SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
358       SortType::TopYear => query
359         .filter(post::published.gt(now - 1.years()))
360         .then_order_by(post_aggregates::score.desc()),
361       SortType::TopMonth => query
362         .filter(post::published.gt(now - 1.months()))
363         .then_order_by(post_aggregates::score.desc()),
364       SortType::TopWeek => query
365         .filter(post::published.gt(now - 1.weeks()))
366         .then_order_by(post_aggregates::score.desc()),
367       SortType::TopDay => query
368         .filter(post::published.gt(now - 1.days()))
369         .then_order_by(post_aggregates::score.desc()),
370     };
371
372     let (limit, offset) = limit_and_offset(self.page, self.limit);
373
374     let res = query
375       .limit(limit)
376       .offset(offset)
377       .filter(post::removed.eq(false))
378       .filter(post::deleted.eq(false))
379       .filter(community::removed.eq(false))
380       .filter(community::deleted.eq(false))
381       .load::<PostViewTuple>(self.conn)?;
382
383     Ok(PostView::to_vec(res))
384   }
385 }
386
387 impl ViewToVec for PostView {
388   type DbTuple = PostViewTuple;
389   fn to_vec(posts: Vec<Self::DbTuple>) -> Vec<Self> {
390     posts
391       .iter()
392       .map(|a| Self {
393         post: a.0.to_owned(),
394         creator: a.1.to_owned(),
395         community: a.2.to_owned(),
396         creator_banned_from_community: a.3.is_some(),
397         counts: a.4.to_owned(),
398         subscribed: a.5.is_some(),
399         saved: a.6.is_some(),
400         read: a.7.is_some(),
401         my_vote: a.8,
402       })
403       .collect::<Vec<Self>>()
404   }
405 }
406
407 #[cfg(test)]
408 mod tests {
409   use crate::{
410     aggregates::post_aggregates::PostAggregates,
411     tests::establish_unpooled_connection,
412     views::post_view::{PostQueryBuilder, PostView},
413     Crud,
414     Likeable,
415     *,
416   };
417   use lemmy_db_schema::source::{community::*, post::*, user::*};
418
419   #[test]
420   fn test_crud() {
421     let conn = establish_unpooled_connection();
422
423     let user_name = "tegan".to_string();
424     let community_name = "test_community_3".to_string();
425     let post_name = "test post 3".to_string();
426
427     let new_user = UserForm {
428       name: user_name.to_owned(),
429       preferred_username: None,
430       password_encrypted: "nope".into(),
431       email: None,
432       matrix_user_id: None,
433       avatar: None,
434       banner: None,
435       published: None,
436       updated: None,
437       admin: false,
438       banned: Some(false),
439       show_nsfw: false,
440       theme: "browser".into(),
441       default_sort_type: SortType::Hot as i16,
442       default_listing_type: ListingType::Subscribed as i16,
443       lang: "browser".into(),
444       show_avatars: true,
445       send_notifications_to_email: false,
446       actor_id: None,
447       bio: None,
448       local: true,
449       private_key: None,
450       public_key: None,
451       last_refreshed_at: None,
452     };
453
454     let inserted_user = User_::create(&conn, &new_user).unwrap();
455
456     let new_community = CommunityForm {
457       name: community_name.to_owned(),
458       title: "nada".to_owned(),
459       description: None,
460       creator_id: inserted_user.id,
461       category_id: 1,
462       removed: None,
463       deleted: None,
464       updated: None,
465       nsfw: false,
466       actor_id: None,
467       local: true,
468       private_key: None,
469       public_key: None,
470       last_refreshed_at: None,
471       published: None,
472       icon: None,
473       banner: None,
474     };
475
476     let inserted_community = Community::create(&conn, &new_community).unwrap();
477
478     let new_post = PostForm {
479       name: post_name.to_owned(),
480       url: None,
481       body: None,
482       creator_id: inserted_user.id,
483       community_id: inserted_community.id,
484       removed: None,
485       deleted: None,
486       locked: None,
487       stickied: None,
488       updated: None,
489       nsfw: false,
490       embed_title: None,
491       embed_description: None,
492       embed_html: None,
493       thumbnail_url: None,
494       ap_id: None,
495       local: true,
496       published: None,
497     };
498
499     let inserted_post = Post::create(&conn, &new_post).unwrap();
500
501     let post_like_form = PostLikeForm {
502       post_id: inserted_post.id,
503       user_id: inserted_user.id,
504       score: 1,
505     };
506
507     let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
508
509     let expected_post_like = PostLike {
510       id: inserted_post_like.id,
511       post_id: inserted_post.id,
512       user_id: inserted_user.id,
513       published: inserted_post_like.published,
514       score: 1,
515     };
516
517     let read_post_listings_with_user = PostQueryBuilder::create(&conn)
518       .listing_type(&ListingType::Community)
519       .sort(&SortType::New)
520       .community_id(inserted_community.id)
521       .my_user_id(inserted_user.id)
522       .list()
523       .unwrap();
524
525     let read_post_listings_no_user = PostQueryBuilder::create(&conn)
526       .listing_type(&ListingType::Community)
527       .sort(&SortType::New)
528       .community_id(inserted_community.id)
529       .list()
530       .unwrap();
531
532     let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
533     let read_post_listing_with_user =
534       PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
535
536     let agg = PostAggregates::read(&conn, inserted_post.id).unwrap();
537
538     // the non user version
539     let expected_post_listing_no_user = PostView {
540       post: Post {
541         id: inserted_post.id,
542         name: post_name.to_owned(),
543         creator_id: inserted_user.id,
544         url: None,
545         body: None,
546         published: inserted_post.published,
547         updated: None,
548         community_id: inserted_community.id,
549         removed: false,
550         deleted: false,
551         locked: false,
552         stickied: false,
553         nsfw: false,
554         embed_title: None,
555         embed_description: None,
556         embed_html: None,
557         thumbnail_url: None,
558         ap_id: inserted_post.ap_id.to_owned(),
559         local: true,
560       },
561       my_vote: None,
562       creator: UserSafe {
563         id: inserted_user.id,
564         name: user_name.to_owned(),
565         preferred_username: None,
566         published: inserted_user.published,
567         avatar: None,
568         actor_id: inserted_user.actor_id.to_owned(),
569         local: true,
570         banned: false,
571         deleted: false,
572         bio: None,
573         banner: None,
574         admin: false,
575         updated: None,
576         matrix_user_id: None,
577       },
578       creator_banned_from_community: false,
579       community: CommunitySafe {
580         id: inserted_community.id,
581         name: community_name.to_owned(),
582         icon: None,
583         removed: false,
584         deleted: false,
585         nsfw: false,
586         actor_id: inserted_community.actor_id.to_owned(),
587         local: true,
588         title: "nada".to_owned(),
589         description: None,
590         creator_id: inserted_user.id,
591         category_id: 1,
592         updated: None,
593         banner: None,
594         published: inserted_community.published,
595       },
596       counts: PostAggregates {
597         id: agg.id,
598         post_id: inserted_post.id,
599         comments: 0,
600         score: 1,
601         upvotes: 1,
602         downvotes: 0,
603         newest_comment_time: inserted_post.published,
604       },
605       subscribed: false,
606       read: false,
607       saved: false,
608     };
609
610     // TODO More needs to be added here
611     let mut expected_post_listing_with_user = expected_post_listing_no_user.to_owned();
612     expected_post_listing_with_user.my_vote = Some(1);
613
614     let like_removed = PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
615     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
616     Community::delete(&conn, inserted_community.id).unwrap();
617     User_::delete(&conn, inserted_user.id).unwrap();
618
619     // The with user
620     assert_eq!(
621       expected_post_listing_with_user,
622       read_post_listings_with_user[0]
623     );
624     assert_eq!(expected_post_listing_with_user, read_post_listing_with_user);
625     assert_eq!(1, read_post_listings_with_user.len());
626
627     // Without the user
628     assert_eq!(expected_post_listing_no_user, read_post_listings_no_user[0]);
629     assert_eq!(expected_post_listing_no_user, read_post_listing_no_user);
630     assert_eq!(1, read_post_listings_no_user.len());
631
632     // assert_eq!(expected_post, inserted_post);
633     // assert_eq!(expected_post, updated_post);
634     assert_eq!(expected_post_like, inserted_post_like);
635     assert_eq!(1, like_removed);
636     assert_eq!(1, num_deleted);
637   }
638 }