mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-03-28 06:05:29 +00:00
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:
parent
304df35f3b
commit
91d10092b6
8 changed files with 182 additions and 40 deletions
|
@ -11,7 +11,7 @@ use lemmy_db_schema::{
|
||||||
},
|
},
|
||||||
traits::Crud,
|
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};
|
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||||
|
|
||||||
pub async fn add_admin(
|
pub async fn add_admin(
|
||||||
|
@ -57,7 +57,12 @@ pub async fn add_admin(
|
||||||
|
|
||||||
ModAdd::create(&mut context.pool(), &form).await?;
|
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 }))
|
Ok(Json(AddAdminResponse { admins }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,40 @@
|
||||||
use actix_web::web::{Data, Json};
|
use actix_web::web::{Data, Json};
|
||||||
use lemmy_api_common::{context::LemmyContext, person::BannedPersonsResponse, utils::is_admin};
|
use lemmy_api_common::{
|
||||||
use lemmy_db_views::structs::{LocalUserView, PersonView};
|
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;
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
|
||||||
pub async fn list_banned_users(
|
pub async fn list_banned_users(
|
||||||
|
data: Json<ListBannedPersons>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
local_user_view: LocalUserView,
|
local_user_view: LocalUserView,
|
||||||
) -> LemmyResult<Json<BannedPersonsResponse>> {
|
) -> LemmyResult<Json<BannedPersonsResponse>> {
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
is_admin(&local_user_view)?;
|
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 }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,10 @@ use lemmy_db_schema::{
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::{LocalUserView, PersonView, SiteView};
|
use lemmy_db_views::{
|
||||||
|
person::person_view::PersonQuery,
|
||||||
|
structs::{LocalUserView, SiteView},
|
||||||
|
};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{LemmyErrorType, LemmyResult},
|
error::{LemmyErrorType, LemmyResult},
|
||||||
VERSION,
|
VERSION,
|
||||||
|
@ -25,7 +28,12 @@ pub async fn leave_admin(
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
// Make sure there isn't just one admin (so if one leaves, there will still be one left)
|
// 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 {
|
if admins.len() == 1 {
|
||||||
Err(LemmyErrorType::CannotLeaveAdmin)?
|
Err(LemmyErrorType::CannotLeaveAdmin)?
|
||||||
}
|
}
|
||||||
|
@ -55,7 +63,12 @@ pub async fn leave_admin(
|
||||||
|
|
||||||
// Reread site and admins
|
// Reread site and admins
|
||||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
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 all_languages = Language::read_all(&mut context.pool()).await?;
|
||||||
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
|
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
|
||||||
|
|
|
@ -344,13 +344,29 @@ pub struct BanPerson {
|
||||||
pub expires: Option<i64>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[cfg_attr(feature = "full", derive(TS))]
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
/// The list of banned persons.
|
/// The list of banned persons.
|
||||||
pub struct BannedPersonsResponse {
|
pub struct BannedPersonsResponse {
|
||||||
pub banned: Vec<PersonView>,
|
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)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
|
|
@ -8,7 +8,10 @@ use lemmy_db_schema::source::{
|
||||||
oauth_provider::OAuthProvider,
|
oauth_provider::OAuthProvider,
|
||||||
tagline::Tagline,
|
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 lemmy_utils::{build_cache, error::LemmyResult, CacheLock, VERSION};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
@ -47,7 +50,12 @@ pub async fn get_site_v4(
|
||||||
|
|
||||||
async fn read_site(context: &LemmyContext) -> LemmyResult<GetSiteResponse> {
|
async fn read_site(context: &LemmyContext) -> LemmyResult<GetSiteResponse> {
|
||||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
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 all_languages = Language::read_all(&mut context.pool()).await?;
|
||||||
let discussion_languages = SiteLanguage::read_local_raw(&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?;
|
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
|
||||||
|
|
|
@ -8,6 +8,8 @@ use crate::{
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
use diesel::{dsl, expression_methods::NullableExpressionMethods};
|
use diesel::{dsl, expression_methods::NullableExpressionMethods};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
use i_love_jesus::CursorKeysModule;
|
||||||
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")]
|
||||||
|
@ -15,10 +17,14 @@ use ts_rs::TS;
|
||||||
|
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
#[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(table_name = person))]
|
||||||
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
#[cfg_attr(feature = "full", cursor_keys_module(name = person_keys))]
|
||||||
/// A person.
|
/// A person.
|
||||||
pub struct Person {
|
pub struct Person {
|
||||||
pub id: PersonId,
|
pub id: PersonId,
|
||||||
|
|
|
@ -268,7 +268,13 @@ pub fn limit_and_offset(
|
||||||
}
|
}
|
||||||
None => 1,
|
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) => {
|
Some(limit) => {
|
||||||
if !(1..=FETCH_LIMIT_MAX).contains(&limit) {
|
if !(1..=FETCH_LIMIT_MAX).contains(&limit) {
|
||||||
return Err(QueryBuilderError(
|
return Err(QueryBuilderError(
|
||||||
|
@ -278,9 +284,7 @@ pub fn limit_and_offset(
|
||||||
limit
|
limit
|
||||||
}
|
}
|
||||||
None => FETCH_LIMIT_DEFAULT,
|
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) {
|
pub fn limit_and_offset_unlimited(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
|
||||||
|
|
|
@ -8,11 +8,39 @@ use diesel::{
|
||||||
SelectableHelper,
|
SelectableHelper,
|
||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
use i_love_jesus::PaginatedQueryBuilder;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::PersonId,
|
newtypes::{PaginationCursor, PersonId},
|
||||||
schema::{local_user, person},
|
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 {
|
impl PersonView {
|
||||||
#[diesel::dsl::auto_type(no_type_alias)]
|
#[diesel::dsl::auto_type(no_type_alias)]
|
||||||
|
@ -37,33 +65,61 @@ impl PersonView {
|
||||||
|
|
||||||
query.first(conn).await
|
query.first(conn).await
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn admins(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
|
#[derive(Default)]
|
||||||
let conn = &mut get_conn(pool).await?;
|
pub struct PersonQuery {
|
||||||
Self::joins()
|
pub admins_only: Option<bool>,
|
||||||
.filter(person::deleted.eq(false))
|
pub banned_only: Option<bool>,
|
||||||
.filter(local_user::admin.eq(true))
|
pub cursor_data: Option<Person>,
|
||||||
.order_by(person::published)
|
pub page_back: Option<bool>,
|
||||||
.select(Self::as_select())
|
pub limit: Option<i64>,
|
||||||
.load::<Self>(conn)
|
}
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
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?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
Self::joins()
|
|
||||||
|
let mut query = PersonView::joins()
|
||||||
.filter(person::deleted.eq(false))
|
.filter(person::deleted.eq(false))
|
||||||
.filter(
|
.select(PersonView::as_select())
|
||||||
person::banned.eq(true).and(
|
.into_boxed();
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
if self.banned_only.unwrap_or_default() {
|
||||||
|
query = query.filter(
|
||||||
|
person::local.and(person::banned).and(
|
||||||
person::ban_expires
|
person::ban_expires
|
||||||
.is_null()
|
.is_null()
|
||||||
.or(person::ban_expires.gt(now().nullable())),
|
.or(person::ban_expires.gt(now().nullable())),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.order_by(person::published)
|
}
|
||||||
.select(Self::as_select())
|
|
||||||
.load::<Self>(conn)
|
if self.admins_only.unwrap_or_default() {
|
||||||
.await
|
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?;
|
.await?;
|
||||||
|
|
||||||
let list = PersonView::banned(pool).await?;
|
let list = PersonQuery {
|
||||||
|
banned_only: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await?;
|
||||||
assert_length!(1, list);
|
assert_length!(1, list);
|
||||||
assert_eq!(list[0].person.id, data.alice.id);
|
assert_eq!(list[0].person.id, data.alice.id);
|
||||||
|
|
||||||
|
@ -198,7 +259,12 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let list = PersonView::admins(pool).await?;
|
let list = PersonQuery {
|
||||||
|
admins_only: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.list(pool)
|
||||||
|
.await?;
|
||||||
assert_length!(1, list);
|
assert_length!(1, list);
|
||||||
assert_eq!(list[0].person.id, data.alice.id);
|
assert_eq!(list[0].person.id, data.alice.id);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue