Adding pagination for GetBannedPersons (#5428)

* Extracting pagination cursor utils into a trait.

- Fixes #5275

* Refactoring to avoid stack overflows.

* Fixing api_common feature.

* Adding pagination for GetBannedPersons.

- Must come after #5424
- Fixes #2847

* Rename the traits and paginationcursor::new

* Using combined trait.

* Removing empty files.

* Merge from main, limit fetch.

* Adding local ban check, and ignore_page_limits.

* Only do page limits if not admin.
This commit is contained in:
Dessalines 2025-03-11 04:41:02 -04:00 committed by GitHub
parent 304df35f3b
commit 91d10092b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 182 additions and 40 deletions

View file

@ -11,7 +11,7 @@ use lemmy_db_schema::{
},
traits::Crud,
};
use lemmy_db_views::structs::{LocalUserView, PersonView};
use lemmy_db_views::{person::person_view::PersonQuery, structs::LocalUserView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
pub async fn add_admin(
@ -57,7 +57,12 @@ pub async fn add_admin(
ModAdd::create(&mut context.pool(), &form).await?;
let admins = PersonView::admins(&mut context.pool()).await?;
let admins = PersonQuery {
admins_only: Some(true),
..Default::default()
}
.list(&mut context.pool())
.await?;
Ok(Json(AddAdminResponse { admins }))
}

View file

@ -1,16 +1,40 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, person::BannedPersonsResponse, utils::is_admin};
use lemmy_db_views::structs::{LocalUserView, PersonView};
use lemmy_api_common::{
context::LemmyContext,
person::{BannedPersonsResponse, ListBannedPersons},
utils::is_admin,
};
use lemmy_db_schema::traits::PaginationCursorBuilder;
use lemmy_db_views::{
person::person_view::PersonQuery,
structs::{LocalUserView, PersonView},
};
use lemmy_utils::error::LemmyResult;
pub async fn list_banned_users(
data: Json<ListBannedPersons>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<BannedPersonsResponse>> {
// Make sure user is an admin
is_admin(&local_user_view)?;
let banned = PersonView::banned(&mut context.pool()).await?;
let cursor_data = if let Some(cursor) = &data.page_cursor {
Some(PersonView::from_cursor(cursor, &mut context.pool()).await?)
} else {
None
};
Ok(Json(BannedPersonsResponse { banned }))
let banned = PersonQuery {
banned_only: Some(true),
cursor_data,
limit: data.limit,
..Default::default()
}
.list(&mut context.pool())
.await?;
let next_page = banned.last().map(PaginationCursorBuilder::to_cursor);
Ok(Json(BannedPersonsResponse { banned, next_page }))
}

View file

@ -12,7 +12,10 @@ use lemmy_db_schema::{
},
traits::Crud,
};
use lemmy_db_views::structs::{LocalUserView, PersonView, SiteView};
use lemmy_db_views::{
person::person_view::PersonQuery,
structs::{LocalUserView, SiteView},
};
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
VERSION,
@ -25,7 +28,12 @@ pub async fn leave_admin(
is_admin(&local_user_view)?;
// Make sure there isn't just one admin (so if one leaves, there will still be one left)
let admins = PersonView::admins(&mut context.pool()).await?;
let admins = PersonQuery {
admins_only: Some(true),
..Default::default()
}
.list(&mut context.pool())
.await?;
if admins.len() == 1 {
Err(LemmyErrorType::CannotLeaveAdmin)?
}
@ -55,7 +63,12 @@ pub async fn leave_admin(
// Reread site and admins
let site_view = SiteView::read_local(&mut context.pool()).await?;
let admins = PersonView::admins(&mut context.pool()).await?;
let admins = PersonQuery {
admins_only: Some(true),
..Default::default()
}
.list(&mut context.pool())
.await?;
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;

View file

@ -344,13 +344,29 @@ pub struct BanPerson {
pub expires: Option<i64>,
}
// TODO, this should be paged, since the list can be quite long.
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List the banned persons.
pub struct ListBannedPersons {
#[cfg_attr(feature = "full", ts(optional))]
pub page_cursor: Option<PaginationCursor>,
#[cfg_attr(feature = "full", ts(optional))]
pub page_back: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The list of banned persons.
pub struct BannedPersonsResponse {
pub banned: Vec<PersonView>,
/// the pagination cursor to use to fetch the next page
#[cfg_attr(feature = "full", ts(optional))]
pub next_page: Option<PaginationCursor>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View file

@ -8,7 +8,10 @@ use lemmy_db_schema::source::{
oauth_provider::OAuthProvider,
tagline::Tagline,
};
use lemmy_db_views::structs::{LocalUserView, PersonView, SiteView};
use lemmy_db_views::{
person::person_view::PersonQuery,
structs::{LocalUserView, SiteView},
};
use lemmy_utils::{build_cache, error::LemmyResult, CacheLock, VERSION};
use std::sync::LazyLock;
@ -47,7 +50,12 @@ pub async fn get_site_v4(
async fn read_site(context: &LemmyContext) -> LemmyResult<GetSiteResponse> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let admins = PersonView::admins(&mut context.pool()).await?;
let admins = PersonQuery {
admins_only: Some(true),
..Default::default()
}
.list(&mut context.pool())
.await?;
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;

View file

@ -8,6 +8,8 @@ use crate::{
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use diesel::{dsl, expression_methods::NullableExpressionMethods};
#[cfg(feature = "full")]
use i_love_jesus::CursorKeysModule;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
@ -15,10 +17,14 @@ use ts_rs::TS;
#[skip_serializing_none]
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Identifiable, TS, CursorKeysModule)
)]
#[cfg_attr(feature = "full", diesel(table_name = person))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
#[cfg_attr(feature = "full", cursor_keys_module(name = person_keys))]
/// A person.
pub struct Person {
pub id: PersonId,

View file

@ -268,7 +268,13 @@ pub fn limit_and_offset(
}
None => 1,
};
let limit = match limit {
let limit = limit_fetch(limit)?;
let offset = limit * (page - 1);
Ok((limit, offset))
}
pub fn limit_fetch(limit: Option<i64>) -> Result<i64, diesel::result::Error> {
Ok(match limit {
Some(limit) => {
if !(1..=FETCH_LIMIT_MAX).contains(&limit) {
return Err(QueryBuilderError(
@ -278,9 +284,7 @@ pub fn limit_and_offset(
limit
}
None => FETCH_LIMIT_DEFAULT,
};
let offset = limit * (page - 1);
Ok((limit, offset))
})
}
pub fn limit_and_offset_unlimited(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {

View file

@ -8,11 +8,39 @@ use diesel::{
SelectableHelper,
};
use diesel_async::RunQueryDsl;
use i_love_jesus::PaginatedQueryBuilder;
use lemmy_db_schema::{
newtypes::PersonId,
newtypes::{PaginationCursor, PersonId},
schema::{local_user, person},
utils::{get_conn, now, DbPool},
source::person::{person_keys as key, Person},
traits::PaginationCursorBuilder,
utils::{get_conn, limit_fetch, now, DbPool},
};
use lemmy_utils::error::LemmyResult;
impl PaginationCursorBuilder for PersonView {
type CursorData = Person;
fn to_cursor(&self) -> PaginationCursor {
PaginationCursor::new('P', self.person.id.0)
}
async fn from_cursor(
cursor: &PaginationCursor,
pool: &mut DbPool<'_>,
) -> LemmyResult<Self::CursorData> {
let conn = &mut get_conn(pool).await?;
let id = cursor.prefix_and_id()?.1;
let token = person::table
.select(Self::CursorData::as_select())
.filter(person::id.eq(id))
.first(conn)
.await?;
Ok(token)
}
}
impl PersonView {
#[diesel::dsl::auto_type(no_type_alias)]
@ -37,33 +65,61 @@ impl PersonView {
query.first(conn).await
}
}
pub async fn admins(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
Self::joins()
.filter(person::deleted.eq(false))
.filter(local_user::admin.eq(true))
.order_by(person::published)
.select(Self::as_select())
.load::<Self>(conn)
.await
}
#[derive(Default)]
pub struct PersonQuery {
pub admins_only: Option<bool>,
pub banned_only: Option<bool>,
pub cursor_data: Option<Person>,
pub page_back: Option<bool>,
pub limit: Option<i64>,
}
pub async fn banned(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
impl PersonQuery {
pub async fn list(self, pool: &mut DbPool<'_>) -> Result<Vec<PersonView>, Error> {
let conn = &mut get_conn(pool).await?;
Self::joins()
let mut query = PersonView::joins()
.filter(person::deleted.eq(false))
.filter(
person::banned.eq(true).and(
.select(PersonView::as_select())
.into_boxed();
// Filters
if self.banned_only.unwrap_or_default() {
query = query.filter(
person::local.and(person::banned).and(
person::ban_expires
.is_null()
.or(person::ban_expires.gt(now().nullable())),
),
)
.order_by(person::published)
.select(Self::as_select())
.load::<Self>(conn)
.await
);
}
if self.admins_only.unwrap_or_default() {
query = query.filter(local_user::admin);
} else {
// Only use page limits if its not an admin fetch
let limit = limit_fetch(self.limit)?;
query = query.limit(limit);
}
let mut query = PaginatedQueryBuilder::new(query);
if self.page_back.unwrap_or_default() {
query = query.before(self.cursor_data).limit_and_offset_from_end();
} else {
query = query.after(self.cursor_data);
}
// Sorting by published
query = query
.then_desc(key::published)
// Tie breaker
.then_desc(key::id);
let res = query.load::<PersonView>(conn).await?;
Ok(res)
}
}
@ -174,7 +230,12 @@ mod tests {
)
.await?;
let list = PersonView::banned(pool).await?;
let list = PersonQuery {
banned_only: Some(true),
..Default::default()
}
.list(pool)
.await?;
assert_length!(1, list);
assert_eq!(list[0].person.id, data.alice.id);
@ -198,7 +259,12 @@ mod tests {
)
.await?;
let list = PersonView::admins(pool).await?;
let list = PersonQuery {
admins_only: Some(true),
..Default::default()
}
.list(pool)
.await?;
assert_length!(1, list);
assert_eq!(list[0].person.id, data.alice.id);