Compare commits

..

No commits in common. "6c8183c1e3783710c279a947a39da3d5b5ec61d8" and "c7c5cf889cdf7226059e92bdd90a18c19c57ae75" have entirely different histories.

7 changed files with 21 additions and 88 deletions

View File

@ -72,8 +72,3 @@ pub struct Page {
pub page: u32, pub page: u32,
pub per_page: u32, pub per_page: u32,
} }
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct UserFilter {
pub username: Option<String>,
}

View File

@ -7,11 +7,7 @@ pub struct AdminRepository<'a> {
} }
impl<'a> AdminRepository<'a> { impl<'a> AdminRepository<'a> {
pub fn paginated_users( pub fn paginated_users(&self, page: Page) -> Result<Vec<models::User>, AuthErr> {
&self, self.store.paginated_users(page)
page: Page,
filter: &models::UserFilter,
) -> Result<Vec<models::User>, AuthErr> {
self.store.paginated_users(page, filter)
} }
} }

View File

@ -66,7 +66,7 @@ pub trait GpodderAuthStore {
fn remove_old_sessions(&self, min_last_seen: DateTime<Utc>) -> Result<usize, AuthErr>; fn remove_old_sessions(&self, min_last_seen: DateTime<Utc>) -> Result<usize, AuthErr>;
/// Return the given page of users, ordered by username /// Return the given page of users, ordered by username
fn paginated_users(&self, page: Page, filter: &UserFilter) -> Result<Vec<User>, AuthErr>; fn paginated_users(&self, page: Page) -> Result<Vec<User>, AuthErr>;
} }
pub trait GpodderDeviceStore { pub trait GpodderDeviceStore {

View File

@ -143,31 +143,16 @@ impl gpodder::GpodderAuthStore for SqliteRepository {
.map_err(AuthErr::from) .map_err(AuthErr::from)
} }
fn paginated_users( fn paginated_users(&self, page: gpodder::Page) -> Result<Vec<gpodder::User>, AuthErr> {
&self, Ok(users::table
page: gpodder::Page, .select(User::as_select())
filter: &gpodder::UserFilter, .order(users::username.asc())
) -> Result<Vec<gpodder::User>, AuthErr> { .offset((page.page * page.per_page) as i64)
(|| { .limit(page.per_page as i64)
let mut query = users::table .get_results(&mut self.pool.get().map_err(DbError::from)?)
.select(User::as_select()) .map_err(DbError::from)?
.order(users::username.asc()) .into_iter()
.offset((page.page * page.per_page) as i64) .map(gpodder::User::from)
.limit(page.per_page as i64) .collect())
.into_boxed();
if let Some(username) = &filter.username {
// Case insensitive by default for SQLite
query = query.filter(users::username.like(format!("%{username}%")));
}
Ok::<_, DbError>(
query
.load_iter(&mut self.pool.get()?)?
.map(|res| res.map(gpodder::User::from))
.collect::<Result<Vec<_>, _>>()?,
)
})()
.map_err(AuthErr::from)
} }
} }

View File

@ -4,7 +4,6 @@ use axum::{
http::HeaderMap, http::HeaderMap,
routing::{delete, get}, routing::{delete, get},
}; };
use serde::Deserialize;
use crate::{ use crate::{
server::{ server::{
@ -23,46 +22,22 @@ pub fn router(ctx: Context) -> Router<Context> {
)) ))
} }
#[derive(Deserialize, Clone)] pub async fn get_users(
struct UserFilter {
username: Option<String>,
}
impl From<UserFilter> for gpodder::UserFilter {
fn from(value: UserFilter) -> Self {
Self {
username: value.username,
}
}
}
impl ToQuery for UserFilter {
fn to_query(self) -> crate::web::Query {
crate::web::Query::default().opt_parameter("username", self.username)
}
}
async fn get_users(
State(ctx): State<Context>, State(ctx): State<Context>,
headers: HeaderMap, headers: HeaderMap,
Extension(session): Extension<gpodder::Session>, Extension(session): Extension<gpodder::Session>,
Query(page): Query<super::Pagination>, Query(page): Query<super::Pagination>,
Query(filter): Query<UserFilter>,
) -> AppResult<TemplateResponse<Page<View>>> { ) -> AppResult<TemplateResponse<Page<View>>> {
let next_page = page.next_page(); let next_page = page.next_page();
let filter_clone = filter.clone();
let user_id = session.user.id; let user_id = session.user.id;
let users = tokio::task::spawn_blocking(move || { let users = tokio::task::spawn_blocking(move || {
ctx.store ctx.store.admin(&session.user)?.paginated_users(page.into())
.admin(&session.user)?
.paginated_users(page.into(), &filter.into())
}) })
.await .await
.unwrap()?; .unwrap()?;
let next_page_query = (users.len() == next_page.per_page as usize) let next_page_query =
.then_some(next_page.to_query().join(filter_clone)); (users.len() == next_page.per_page as usize).then_some(next_page.to_query());
Ok(View::Users(users, user_id, next_page_query) Ok(View::Users(users, user_id, next_page_query)
.page(&headers) .page(&headers)

View File

@ -23,15 +23,6 @@ impl Query {
self self
} }
/// Convenience method for adding possibly empty parameter values from options
pub fn opt_parameter(self, key: impl ToString, value: Option<impl ToString>) -> Self {
if let Some(value) = value {
self.parameter(key, value)
} else {
self
}
}
} }
/// Allows objects to be converted into queries /// Allows objects to be converted into queries

View File

@ -1,24 +1,15 @@
<h1>Users</h1> <h1>Users</h1>
<input <table>
type="text" id="username" name="username"
hx-get="/users"
hx-target="#users > tbody"
hx-swap="innerHTML"
hx-select="table > tbody > tr"
hx-trigger="input changed delay:500ms"
placeholder="Search..."
/>
<table id="users">
<thead> <thead>
<th>Username</th> <th>Username</th>
</thead> </thead>
<tbody> <tbody>
{%- for user in users %} {% for user in users %}
<tr> <tr>
<th>{{ user.username }}</th> <th>{{ user.username }}</th>
</tr> </tr>
{% endfor -%} {% endfor %}
{%- if next_page_query %} {%- if next_page_query %}
<tr <tr