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 <tyhou13@gmx.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
Steven Vergenz 2024-10-15 14:15:43 -07:00 committed by GitHub
parent 9509ef1706
commit 859dfb3f81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 162 additions and 25 deletions

View file

@ -3,9 +3,13 @@ use lemmy_db_schema::{
source::site::Site, source::site::Site,
CommunityVisibility, CommunityVisibility,
ListingType, 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::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
#[cfg(feature = "full")] #[cfg(feature = "full")]
@ -74,7 +78,7 @@ pub struct CommunityResponse {
/// Fetches a list of communities. /// Fetches a list of communities.
pub struct ListCommunities { pub struct ListCommunities {
pub type_: Option<ListingType>, pub type_: Option<ListingType>,
pub sort: Option<PostSortType>, pub sort: Option<CommunitySortType>,
pub show_nsfw: Option<bool>, pub show_nsfw: Option<bool>,
pub page: Option<i64>, pub page: Option<i64>,
pub limit: Option<i64>, pub limit: Option<i64>,

View file

@ -12,7 +12,11 @@ use lemmy_db_views::{
post_view::PostQuery, post_view::PostQuery,
structs::{LocalUserView, SiteView}, 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; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -102,7 +106,7 @@ pub async fn search(
}; };
let community_query = CommunityQuery { let community_query = CommunityQuery {
sort, sort: sort.map(CommunitySortType::from),
listing_type, listing_type,
search_term: Some(q.clone()), search_term: Some(q.clone()),
title_only, title_only,

View file

@ -1,4 +1,4 @@
use crate::structs::{CommunityModeratorView, CommunityView, PersonView}; use crate::structs::{CommunityModeratorView, CommunitySortType, CommunityView, PersonView};
use diesel::{ use diesel::{
pg::Pg, pg::Pg,
result::Error, result::Error,
@ -22,7 +22,16 @@ use lemmy_db_schema::{
instance_block, instance_block,
}, },
source::{community::CommunityFollower, local_user::LocalUser, site::Site}, 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, ListingType,
PostSortType, PostSortType,
}; };
@ -103,7 +112,7 @@ fn queries<'a>() -> Queries<
}; };
let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { 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 // The left join below will return None in this case
let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1)); 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()), TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()),
TopWeek => query = query.order_by(community_aggregates::users_active_week.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 { if let Some(listing_type) = options.listing_type {
@ -228,10 +239,36 @@ impl CommunityView {
} }
} }
impl From<PostSortType> 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)] #[derive(Default)]
pub struct CommunityQuery<'a> { pub struct CommunityQuery<'a> {
pub listing_type: Option<ListingType>, pub listing_type: Option<ListingType>,
pub sort: Option<PostSortType>, pub sort: Option<CommunitySortType>,
pub local_user: Option<&'a LocalUser>, pub local_user: Option<&'a LocalUser>,
pub search_term: Option<String>, pub search_term: Option<String>,
pub title_only: Option<bool>, pub title_only: Option<bool>,
@ -250,7 +287,10 @@ impl<'a> CommunityQuery<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{community_view::CommunityQuery, structs::CommunityView}; use crate::{
community_view::CommunityQuery,
structs::{CommunitySortType, CommunityView},
};
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityInsertForm, CommunityUpdateForm}, community::{Community, CommunityInsertForm, CommunityUpdateForm},
@ -270,7 +310,7 @@ mod tests {
struct Data { struct Data {
inserted_instance: Instance, inserted_instance: Instance,
local_user: LocalUser, local_user: LocalUser,
inserted_community: Community, inserted_communities: [Community; 3],
site: Site, site: Site,
} }
@ -286,13 +326,38 @@ mod tests {
let local_user_form = LocalUserInsertForm::test_form(inserted_person.id); let local_user_form = LocalUserInsertForm::test_form(inserted_person.id);
let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?; let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
let new_community = CommunityInsertForm::new( let inserted_communities = [
inserted_instance.id, Community::create(
"test_community_3".to_string(), pool,
"nada".to_owned(), &CommunityInsertForm::new(
"pubkey".to_string(), inserted_instance.id,
); "test_community_1".to_string(),
let inserted_community = Community::create(pool, &new_community).await?; "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 url = Url::parse("http://example.com")?;
let site = Site { let site = Site {
@ -316,13 +381,15 @@ mod tests {
Ok(Data { Ok(Data {
inserted_instance, inserted_instance,
local_user, local_user,
inserted_community, inserted_communities,
site, site,
}) })
} }
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { 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?; Person::delete(pool, data.local_user.person_id).await?;
Instance::delete(pool, data.inserted_instance.id).await?; Instance::delete(pool, data.inserted_instance.id).await?;
@ -338,7 +405,7 @@ mod tests {
Community::update( Community::update(
pool, pool,
data.inserted_community.id, data.inserted_communities[0].id,
&CommunityUpdateForm { &CommunityUpdateForm {
visibility: Some(CommunityVisibility::LocalOnly), visibility: Some(CommunityVisibility::LocalOnly),
..Default::default() ..Default::default()
@ -351,7 +418,10 @@ mod tests {
} }
.list(&data.site, pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!(0, unauthenticated_query.len()); assert_eq!(
data.inserted_communities.len() - 1,
unauthenticated_query.len()
);
let authenticated_query = CommunityQuery { let authenticated_query = CommunityQuery {
local_user: Some(&data.local_user), local_user: Some(&data.local_user),
@ -359,15 +429,15 @@ mod tests {
} }
.list(&data.site, pool) .list(&data.site, pool)
.await?; .await?;
assert_eq!(1, authenticated_query.len()); assert_eq!(data.inserted_communities.len(), authenticated_query.len());
let unauthenticated_community = 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()); assert!(unauthenticated_community.is_err());
let authenticated_community = CommunityView::read( let authenticated_community = CommunityView::read(
pool, pool,
data.inserted_community.id, data.inserted_communities[0].id,
Some(&data.local_user), Some(&data.local_user),
false, false,
) )
@ -376,4 +446,34 @@ mod tests {
cleanup(data, pool).await 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
}
} }

View file

@ -59,6 +59,35 @@ pub struct CommunityView {
pub banned_from_community: bool, 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] #[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", derive(TS, Queryable))]