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