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