From 859dfb3f81235de229945d1d4f41717dc320442b Mon Sep 17 00:00:00 2001 From: Steven Vergenz <1882376+stevenvergenz@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:15:43 -0700 Subject: [PATCH] Add community alphabetic sorting (#5056) * Started * Finished? Need to write tests * Formatting * Formatting * Formatting * Write tests * Formatting * Formatting * Formatting * Unnecessary lifetime * Safety * Unwrap * Formatting * Formatting * Fix local_only test * Formatting * Name consistency * Adding lower to community name sort. --------- Co-authored-by: Dessalines Co-authored-by: Dessalines --- crates/api_common/src/community.rs | 10 +- crates/apub/src/api/search.rs | 8 +- crates/db_views_actor/src/community_view.rs | 140 +++++++++++++++++--- crates/db_views_actor/src/structs.rs | 29 ++++ 4 files changed, 162 insertions(+), 25 deletions(-) diff --git a/crates/api_common/src/community.rs b/crates/api_common/src/community.rs index 25a2ca0d4..f8e741a58 100644 --- a/crates/api_common/src/community.rs +++ b/crates/api_common/src/community.rs @@ -3,9 +3,13 @@ use lemmy_db_schema::{ source::site::Site, CommunityVisibility, ListingType, - PostSortType, }; -use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView, PersonView}; +use lemmy_db_views_actor::structs::{ + CommunityModeratorView, + CommunitySortType, + CommunityView, + PersonView, +}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -74,7 +78,7 @@ pub struct CommunityResponse { /// Fetches a list of communities. pub struct ListCommunities { pub type_: Option, - pub sort: Option, + pub sort: Option, pub show_nsfw: Option, pub page: Option, pub limit: Option, diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index d9ae20ede..cdc9bc55e 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -12,7 +12,11 @@ use lemmy_db_views::{ post_view::PostQuery, structs::{LocalUserView, SiteView}, }; -use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery}; +use lemmy_db_views_actor::{ + community_view::CommunityQuery, + person_view::PersonQuery, + structs::CommunitySortType, +}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -102,7 +106,7 @@ pub async fn search( }; let community_query = CommunityQuery { - sort, + sort: sort.map(CommunitySortType::from), listing_type, search_term: Some(q.clone()), title_only, diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index dec4537e0..9ff6fadce 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -1,4 +1,4 @@ -use crate::structs::{CommunityModeratorView, CommunityView, PersonView}; +use crate::structs::{CommunityModeratorView, CommunitySortType, CommunityView, PersonView}; use diesel::{ pg::Pg, result::Error, @@ -22,7 +22,16 @@ use lemmy_db_schema::{ instance_block, }, source::{community::CommunityFollower, local_user::LocalUser, site::Site}, - utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{ + functions::lower, + fuzzy_search, + limit_and_offset, + DbConn, + DbPool, + ListFn, + Queries, + ReadFn, + }, ListingType, PostSortType, }; @@ -103,7 +112,7 @@ fn queries<'a>() -> Queries< }; let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { - use PostSortType::*; + use CommunitySortType::*; // The left join below will return None in this case let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); @@ -148,6 +157,8 @@ fn queries<'a>() -> Queries< } TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()), TopWeek => query = query.order_by(community_aggregates::users_active_week.desc()), + NameAsc => query = query.order_by(lower(community::name).asc()), + NameDesc => query = query.order_by(lower(community::name).desc()), }; if let Some(listing_type) = options.listing_type { @@ -228,10 +239,36 @@ impl CommunityView { } } +impl From for CommunitySortType { + fn from(value: PostSortType) -> Self { + match value { + PostSortType::Active => Self::Active, + PostSortType::Hot => Self::Hot, + PostSortType::New => Self::New, + PostSortType::Old => Self::Old, + PostSortType::TopDay => Self::TopDay, + PostSortType::TopWeek => Self::TopWeek, + PostSortType::TopMonth => Self::TopMonth, + PostSortType::TopYear => Self::TopYear, + PostSortType::TopAll => Self::TopAll, + PostSortType::MostComments => Self::MostComments, + PostSortType::NewComments => Self::NewComments, + PostSortType::TopHour => Self::TopHour, + PostSortType::TopSixHour => Self::TopSixHour, + PostSortType::TopTwelveHour => Self::TopTwelveHour, + PostSortType::TopThreeMonths => Self::TopThreeMonths, + PostSortType::TopSixMonths => Self::TopSixMonths, + PostSortType::TopNineMonths => Self::TopNineMonths, + PostSortType::Controversial => Self::Controversial, + PostSortType::Scaled => Self::Scaled, + } + } +} + #[derive(Default)] pub struct CommunityQuery<'a> { pub listing_type: Option, - pub sort: Option, + pub sort: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub title_only: Option, @@ -250,7 +287,10 @@ impl<'a> CommunityQuery<'a> { #[cfg(test)] mod tests { - use crate::{community_view::CommunityQuery, structs::CommunityView}; + use crate::{ + community_view::CommunityQuery, + structs::{CommunitySortType, CommunityView}, + }; use lemmy_db_schema::{ source::{ community::{Community, CommunityInsertForm, CommunityUpdateForm}, @@ -270,7 +310,7 @@ mod tests { struct Data { inserted_instance: Instance, local_user: LocalUser, - inserted_community: Community, + inserted_communities: [Community; 3], site: Site, } @@ -286,13 +326,38 @@ mod tests { let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test_community_3".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; + let inserted_communities = [ + Community::create( + pool, + &CommunityInsertForm::new( + inserted_instance.id, + "test_community_1".to_string(), + "nada1".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + Community::create( + pool, + &CommunityInsertForm::new( + inserted_instance.id, + "test_community_2".to_string(), + "nada2".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + Community::create( + pool, + &CommunityInsertForm::new( + inserted_instance.id, + "test_community_3".to_string(), + "nada3".to_owned(), + "pubkey".to_string(), + ), + ) + .await?, + ]; let url = Url::parse("http://example.com")?; let site = Site { @@ -316,13 +381,15 @@ mod tests { Ok(Data { inserted_instance, local_user, - inserted_community, + inserted_communities, site, }) } async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { - Community::delete(pool, data.inserted_community.id).await?; + for Community { id, .. } in data.inserted_communities { + Community::delete(pool, id).await?; + } Person::delete(pool, data.local_user.person_id).await?; Instance::delete(pool, data.inserted_instance.id).await?; @@ -338,7 +405,7 @@ mod tests { Community::update( pool, - data.inserted_community.id, + data.inserted_communities[0].id, &CommunityUpdateForm { visibility: Some(CommunityVisibility::LocalOnly), ..Default::default() @@ -351,7 +418,10 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(0, unauthenticated_query.len()); + assert_eq!( + data.inserted_communities.len() - 1, + unauthenticated_query.len() + ); let authenticated_query = CommunityQuery { local_user: Some(&data.local_user), @@ -359,15 +429,15 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(1, authenticated_query.len()); + assert_eq!(data.inserted_communities.len(), authenticated_query.len()); let unauthenticated_community = - CommunityView::read(pool, data.inserted_community.id, None, false).await; + CommunityView::read(pool, data.inserted_communities[0].id, None, false).await; assert!(unauthenticated_community.is_err()); let authenticated_community = CommunityView::read( pool, - data.inserted_community.id, + data.inserted_communities[0].id, Some(&data.local_user), false, ) @@ -376,4 +446,34 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn community_sort_name() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + let query = CommunityQuery { + sort: Some(CommunitySortType::NameAsc), + ..Default::default() + }; + let communities = query.list(&data.site, pool).await?; + for (i, c) in communities.iter().enumerate().skip(1) { + let prev = communities.get(i - 1).expect("No previous community?"); + assert!(c.community.title.cmp(&prev.community.title).is_ge()); + } + + let query = CommunityQuery { + sort: Some(CommunitySortType::NameDesc), + ..Default::default() + }; + let communities = query.list(&data.site, pool).await?; + for (i, c) in communities.iter().enumerate().skip(1) { + let prev = communities.get(i - 1).expect("No previous community?"); + assert!(c.community.title.cmp(&prev.community.title).is_le()); + } + + cleanup(data, pool).await + } } diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index 2992d575d..ecf9ba11d 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -59,6 +59,35 @@ pub struct CommunityView { pub banned_from_community: bool, } +/// The community sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub enum CommunitySortType { + #[default] + Active, + Hot, + New, + Old, + TopDay, + TopWeek, + TopMonth, + TopYear, + TopAll, + MostComments, + NewComments, + TopHour, + TopSixHour, + TopTwelveHour, + TopThreeMonths, + TopSixMonths, + TopNineMonths, + Controversial, + Scaled, + NameAsc, + NameDesc, +} + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))]