From caedb7fcc459ad93db9e3e95ff6a18b57154cdde Mon Sep 17 00:00:00 2001 From: Dessalines Date: Sat, 5 Dec 2020 22:49:15 -0500 Subject: [PATCH] Community view halfway done. --- lemmy_db/src/community.rs | 65 +++++++ lemmy_db/src/user.rs | 84 ++++---- lemmy_db/src/views/community_view.rs | 276 +++++++++++++++++++++++++-- 3 files changed, 367 insertions(+), 58 deletions(-) diff --git a/lemmy_db/src/community.rs b/lemmy_db/src/community.rs index 971bbf248..8638f1d64 100644 --- a/lemmy_db/src/community.rs +++ b/lemmy_db/src/community.rs @@ -32,6 +32,71 @@ pub struct Community { pub banner: Option, } +/// 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, + pub category_id: i32, + pub creator_id: i32, + pub removed: bool, + pub published: chrono::NaiveDateTime, + pub updated: Option, + pub deleted: bool, + pub nsfw: bool, + pub actor_id: String, + pub local: bool, + pub icon: Option, + pub banner: Option, +} + +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 { diff --git a/lemmy_db/src/user.rs b/lemmy_db/src/user.rs index 389554263..b2cb0e17e 100644 --- a/lemmy_db/src/user.rs +++ b/lemmy_db/src/user.rs @@ -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}; diff --git a/lemmy_db/src/views/community_view.rs b/lemmy_db/src/views/community_view.rs index 2972b1fe5..2ab351f40 100644 --- a/lemmy_db/src/views/community_view.rs +++ b/lemmy_db/src/views/community_view.rs @@ -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, ) -> Result { - 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::(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, + )>(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, + Integer, + Integer, + Bool, + Timestamp, + Nullable, + Bool, + Bool, + Text, + Bool, + Nullable, + Nullable, + ), + ( + Integer, + Text, + Nullable, + Nullable, + Bool, + Bool, + Timestamp, + Nullable, + Nullable, + Text, + Nullable, + Bool, + Nullable, + Bool, + ), + (Integer, Text), + (Integer, Integer, BigInt, BigInt, BigInt), + Nullable<(Integer, Integer, Integer, Timestamp, Nullable)>, + ), + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join< + JoinOn< + Join, + diesel::expression::operators::Eq< + diesel::expression::nullable::Nullable, + diesel::expression::nullable::Nullable, + >, + >, + category::table, + Inner, + >, + diesel::expression::operators::Eq< + diesel::expression::nullable::Nullable, + diesel::expression::nullable::Nullable, + >, + >, + community_aggregates::table, + Inner, + >, + diesel::expression::operators::Eq< + diesel::expression::nullable::Nullable, + diesel::expression::nullable::Nullable, + >, + >, + 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, + >, + >, + >, + Pg, + >; +} + +pub struct CommunityQueryBuilder<'a> { + conn: &'a PgConnection, + query: join_types::BoxedCommunityJoin<'a>, + sort: &'a SortType, + show_nsfw: bool, + search_term: Option, + page: Option, + limit: Option, +} + +impl<'a> CommunityQueryBuilder<'a> { + pub fn create(conn: &'a PgConnection, my_user_id: Option) -> 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>(mut self, search_term: T) -> Self { + self.search_term = search_term.get_optional(); + self + } + + pub fn page>(mut self, page: T) -> Self { + self.page = page.get_optional(); + self + } + + pub fn limit>(mut self, limit: T) -> Self { + self.limit = limit.get_optional(); + self + } + + pub fn list(self) -> Result, 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, + )>(self.conn)?; + + Ok(to_vec(res)) + } +} + +fn to_vec( + users: Vec<( + CommunitySafe, + UserSafe, + Category, + CommunityAggregates, + Option, + )>, +) -> Vec { + 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::>() +}