Community view halfway done.
authorDessalines <tyhou13@gmx.com>
Sun, 6 Dec 2020 03:49:15 +0000 (22:49 -0500)
committerDessalines <tyhou13@gmx.com>
Sun, 6 Dec 2020 03:49:15 +0000 (22:49 -0500)
lemmy_db/src/community.rs
lemmy_db/src/user.rs
lemmy_db/src/views/community_view.rs

index 971bbf248889e91b561ca3e39a4313c272d50c42..8638f1d648706500523d8ed75994574f2d847171 100644 (file)
@@ -32,6 +32,71 @@ pub struct Community {
   pub banner: Option<String>,
 }
 
+/// A safe representation of community, without the sensitive info
+#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
+#[table_name = "community"]
+pub struct CommunitySafe {
+  pub id: i32,
+  pub name: String,
+  pub title: String,
+  pub description: Option<String>,
+  pub category_id: i32,
+  pub creator_id: i32,
+  pub removed: bool,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+  pub deleted: bool,
+  pub nsfw: bool,
+  pub actor_id: String,
+  pub local: bool,
+  pub icon: Option<String>,
+  pub banner: Option<String>,
+}
+
+mod safe_type {
+  use crate::{community::Community, schema::community::columns::*, ToSafe};
+  type Columns = (
+    id,
+    name,
+    title,
+    description,
+    category_id,
+    creator_id,
+    removed,
+    published,
+    updated,
+    deleted,
+    nsfw,
+    actor_id,
+    local,
+    icon,
+    banner,
+  );
+
+  impl ToSafe for Community {
+    type SafeColumns = Columns;
+    fn safe_columns_tuple() -> Self::SafeColumns {
+      (
+        id,
+        name,
+        title,
+        description,
+        category_id,
+        creator_id,
+        removed,
+        published,
+        updated,
+        deleted,
+        nsfw,
+        actor_id,
+        local,
+        icon,
+        banner,
+      )
+    }
+  }
+}
+
 #[derive(Insertable, AsChangeset, Debug)]
 #[table_name = "community"]
 pub struct CommunityForm {
index 3895542634831add433f3db0905f48bfb6176ed7..b2cb0e17e7cac2cd760c7aa9d702e5e443381c41 100644 (file)
@@ -60,6 +60,48 @@ pub struct UserSafe {
   pub deleted: bool,
 }
 
+mod safe_type {
+  use crate::{schema::user_::columns::*, user::User_, ToSafe};
+  type Columns = (
+    id,
+    name,
+    preferred_username,
+    avatar,
+    admin,
+    banned,
+    published,
+    updated,
+    matrix_user_id,
+    actor_id,
+    bio,
+    local,
+    banner,
+    deleted,
+  );
+
+  impl ToSafe for User_ {
+    type SafeColumns = Columns;
+    fn safe_columns_tuple() -> Self::SafeColumns {
+      (
+        id,
+        name,
+        preferred_username,
+        avatar,
+        admin,
+        banned,
+        published,
+        updated,
+        matrix_user_id,
+        actor_id,
+        bio,
+        local,
+        banner,
+        deleted,
+      )
+    }
+  }
+}
+
 #[derive(Insertable, AsChangeset, Clone)]
 #[table_name = "user_"]
 pub struct UserForm {
@@ -222,48 +264,6 @@ impl User_ {
   }
 }
 
-mod safe_type {
-  use crate::{schema::user_::columns::*, user::User_, ToSafe};
-  type Columns = (
-    id,
-    name,
-    preferred_username,
-    avatar,
-    admin,
-    banned,
-    published,
-    updated,
-    matrix_user_id,
-    actor_id,
-    bio,
-    local,
-    banner,
-    deleted,
-  );
-
-  impl ToSafe for User_ {
-    type SafeColumns = Columns;
-    fn safe_columns_tuple() -> Self::SafeColumns {
-      (
-        id,
-        name,
-        preferred_username,
-        avatar,
-        admin,
-        banned,
-        published,
-        updated,
-        matrix_user_id,
-        actor_id,
-        bio,
-        local,
-        banner,
-        deleted,
-      )
-    }
-  }
-}
-
 #[cfg(test)]
 mod tests {
   use crate::{tests::establish_unpooled_connection, user::*, ListingType, SortType};
index 2972b1fe51e14e42a852ad1a81e321debc15dd89..2ab351f400090cca7c93da39584c4602ddbae142 100644 (file)
@@ -1,9 +1,13 @@
 use crate::{
   aggregates::community_aggregates::CommunityAggregates,
   category::Category,
-  community::{Community, CommunityFollower},
+  community::{Community, CommunityFollower, CommunitySafe},
+  fuzzy_search,
+  limit_and_offset,
   schema::{category, community, community_aggregates, community_follower, user_},
   user::{UserSafe, User_},
+  MaybeOptional,
+  SortType,
   ToSafe,
 };
 use diesel::{result::Error, *};
@@ -11,7 +15,7 @@ use serde::Serialize;
 
 #[derive(Debug, Serialize, Clone)]
 pub struct CommunityView {
-  pub community: Community,
+  pub community: CommunitySafe,
   pub creator: UserSafe,
   pub category: Category,
   pub subscribed: bool,
@@ -24,36 +28,276 @@ impl CommunityView {
     community_id: i32,
     my_user_id: Option<i32>,
   ) -> Result<Self, Error> {
-    let subscribed = match my_user_id {
-      Some(user_id) => {
-        let res = community_follower::table
-          .filter(community_follower::community_id.eq(community_id))
-          .filter(community_follower::user_id.eq(user_id))
-          .get_result::<CommunityFollower>(conn);
-        res.is_ok()
-      }
-      None => false,
-    };
+    // The left join below will return None in this case
+    let user_id_join = my_user_id.unwrap_or(-1);
 
-    let (community, creator, category, counts) = community::table
+    let (community, creator, category, counts, subscribed) = community::table
       .find(community_id)
       .inner_join(user_::table)
       .inner_join(category::table)
       .inner_join(community_aggregates::table)
+      .left_join(
+        community_follower::table.on(
+          community::id
+            .eq(community_follower::community_id)
+            .and(community_follower::user_id.eq(user_id_join)),
+        ),
+      )
       .select((
-        community::all_columns,
+        Community::safe_columns_tuple(),
         User_::safe_columns_tuple(),
         category::all_columns,
         community_aggregates::all_columns,
+        community_follower::all_columns.nullable(),
       ))
-      .first::<(Community, UserSafe, Category, CommunityAggregates)>(conn)?;
+      .first::<(
+        CommunitySafe,
+        UserSafe,
+        Category,
+        CommunityAggregates,
+        Option<CommunityFollower>,
+      )>(conn)?;
 
     Ok(CommunityView {
       community,
       creator,
       category,
-      subscribed,
+      subscribed: subscribed.is_some(),
       counts,
     })
   }
 }
+
+mod join_types {
+  use crate::schema::{category, community, community_aggregates, community_follower, user_};
+  use diesel::{
+    pg::Pg,
+    query_builder::BoxedSelectStatement,
+    query_source::joins::{Inner, Join, JoinOn, LeftOuter},
+    sql_types::*,
+  };
+
+  /// TODO awful, but necessary because of the boxed join
+  pub(super) type BoxedCommunityJoin<'a> = BoxedSelectStatement<
+    'a,
+    (
+      (
+        Integer,
+        Text,
+        Text,
+        Nullable<Text>,
+        Integer,
+        Integer,
+        Bool,
+        Timestamp,
+        Nullable<Timestamp>,
+        Bool,
+        Bool,
+        Text,
+        Bool,
+        Nullable<Text>,
+        Nullable<Text>,
+      ),
+      (
+        Integer,
+        Text,
+        Nullable<Text>,
+        Nullable<Text>,
+        Bool,
+        Bool,
+        Timestamp,
+        Nullable<Timestamp>,
+        Nullable<Text>,
+        Text,
+        Nullable<Text>,
+        Bool,
+        Nullable<Text>,
+        Bool,
+      ),
+      (Integer, Text),
+      (Integer, Integer, BigInt, BigInt, BigInt),
+      Nullable<(Integer, Integer, Integer, Timestamp, Nullable<Bool>)>,
+    ),
+    JoinOn<
+      Join<
+        JoinOn<
+          Join<
+            JoinOn<
+              Join<
+                JoinOn<
+                  Join<community::table, user_::table, Inner>,
+                  diesel::expression::operators::Eq<
+                    diesel::expression::nullable::Nullable<community::columns::creator_id>,
+                    diesel::expression::nullable::Nullable<user_::columns::id>,
+                  >,
+                >,
+                category::table,
+                Inner,
+              >,
+              diesel::expression::operators::Eq<
+                diesel::expression::nullable::Nullable<community::columns::category_id>,
+                diesel::expression::nullable::Nullable<category::columns::id>,
+              >,
+            >,
+            community_aggregates::table,
+            Inner,
+          >,
+          diesel::expression::operators::Eq<
+            diesel::expression::nullable::Nullable<community_aggregates::columns::community_id>,
+            diesel::expression::nullable::Nullable<community::columns::id>,
+          >,
+        >,
+        community_follower::table,
+        LeftOuter,
+      >,
+      diesel::expression::operators::And<
+        diesel::expression::operators::Eq<
+          community::columns::id,
+          community_follower::columns::community_id,
+        >,
+        diesel::expression::operators::Eq<
+          community_follower::columns::user_id,
+          diesel::expression::bound::Bound<diesel::sql_types::Integer, i32>,
+        >,
+      >,
+    >,
+    Pg,
+  >;
+}
+
+pub struct CommunityQueryBuilder<'a> {
+  conn: &'a PgConnection,
+  query: join_types::BoxedCommunityJoin<'a>,
+  sort: &'a SortType,
+  show_nsfw: bool,
+  search_term: Option<String>,
+  page: Option<i64>,
+  limit: Option<i64>,
+}
+
+impl<'a> CommunityQueryBuilder<'a> {
+  pub fn create(conn: &'a PgConnection, my_user_id: Option<i32>) -> Self {
+    // The left join below will return None in this case
+    let user_id_join = my_user_id.unwrap_or(-1);
+
+    let query = community::table
+      .inner_join(user_::table)
+      .inner_join(category::table)
+      .inner_join(community_aggregates::table)
+      .left_join(
+        community_follower::table.on(
+          community::id
+            .eq(community_follower::community_id)
+            .and(community_follower::user_id.eq(user_id_join)),
+        ),
+      )
+      .select((
+        Community::safe_columns_tuple(),
+        User_::safe_columns_tuple(),
+        category::all_columns,
+        community_aggregates::all_columns,
+        community_follower::all_columns.nullable(),
+      ))
+      .into_boxed();
+
+    CommunityQueryBuilder {
+      conn,
+      query,
+      sort: &SortType::Hot,
+      show_nsfw: true,
+      search_term: None,
+      page: None,
+      limit: None,
+    }
+  }
+
+  pub fn sort(mut self, sort: &'a SortType) -> Self {
+    self.sort = sort;
+    self
+  }
+
+  pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
+    self.show_nsfw = show_nsfw;
+    self
+  }
+
+  pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
+    self.search_term = search_term.get_optional();
+    self
+  }
+
+  pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
+    self.page = page.get_optional();
+    self
+  }
+
+  pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
+    self.limit = limit.get_optional();
+    self
+  }
+
+  pub fn list(self) -> Result<Vec<CommunityView>, Error> {
+    let mut query = self.query;
+
+    if let Some(search_term) = self.search_term {
+      let searcher = fuzzy_search(&search_term);
+      query = query
+        .filter(community::name.ilike(searcher.to_owned()))
+        .or_filter(community::title.ilike(searcher.to_owned()))
+        .or_filter(community::description.ilike(searcher));
+    };
+
+    match self.sort {
+      SortType::New => query = query.order_by(community::published.desc()),
+      SortType::TopAll => query = query.order_by(community_aggregates::subscribers.desc()),
+      // Covers all other sorts, including hot
+      _ => {
+        query = query
+          // TODO do custom sql function for hot_rank
+          // .order_by(hot_rank.desc())
+          .then_order_by(community_aggregates::subscribers.desc())
+      }
+    };
+
+    if !self.show_nsfw {
+      query = query.filter(community::nsfw.eq(false));
+    };
+
+    let (limit, offset) = limit_and_offset(self.page, self.limit);
+    let res = query
+      .limit(limit)
+      .offset(offset)
+      .filter(community::removed.eq(false))
+      .filter(community::deleted.eq(false))
+      .load::<(
+        CommunitySafe,
+        UserSafe,
+        Category,
+        CommunityAggregates,
+        Option<CommunityFollower>,
+      )>(self.conn)?;
+
+    Ok(to_vec(res))
+  }
+}
+
+fn to_vec(
+  users: Vec<(
+    CommunitySafe,
+    UserSafe,
+    Category,
+    CommunityAggregates,
+    Option<CommunityFollower>,
+  )>,
+) -> Vec<CommunityView> {
+  users
+    .iter()
+    .map(|a| CommunityView {
+      community: a.0.to_owned(),
+      creator: a.1.to_owned(),
+      category: a.2.to_owned(),
+      counts: a.3.to_owned(),
+      subscribed: a.4.is_some(),
+    })
+    .collect::<Vec<CommunityView>>()
+}