Compare commits
	
		
			2 Commits 
		
	
	
		
			c7c5cf889c
			...
			6c8183c1e3
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 6c8183c1e3 | |
|  | fce301080c | 
|  | @ -72,3 +72,8 @@ 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>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -7,7 +7,11 @@ pub struct AdminRepository<'a> { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<'a> AdminRepository<'a> { | impl<'a> AdminRepository<'a> { | ||||||
|     pub fn paginated_users(&self, page: Page) -> Result<Vec<models::User>, AuthErr> { |     pub fn paginated_users( | ||||||
|         self.store.paginated_users(page) |         &self, | ||||||
|  |         page: Page, | ||||||
|  |         filter: &models::UserFilter, | ||||||
|  |     ) -> Result<Vec<models::User>, AuthErr> { | ||||||
|  |         self.store.paginated_users(page, filter) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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) -> Result<Vec<User>, AuthErr>; |     fn paginated_users(&self, page: Page, filter: &UserFilter) -> Result<Vec<User>, AuthErr>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub trait GpodderDeviceStore { | pub trait GpodderDeviceStore { | ||||||
|  |  | ||||||
|  | @ -143,16 +143,31 @@ impl gpodder::GpodderAuthStore for SqliteRepository { | ||||||
|         .map_err(AuthErr::from) |         .map_err(AuthErr::from) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn paginated_users(&self, page: gpodder::Page) -> Result<Vec<gpodder::User>, AuthErr> { |     fn paginated_users( | ||||||
|         Ok(users::table |         &self, | ||||||
|             .select(User::as_select()) |         page: gpodder::Page, | ||||||
|             .order(users::username.asc()) |         filter: &gpodder::UserFilter, | ||||||
|             .offset((page.page * page.per_page) as i64) |     ) -> Result<Vec<gpodder::User>, AuthErr> { | ||||||
|             .limit(page.per_page as i64) |         (|| { | ||||||
|             .get_results(&mut self.pool.get().map_err(DbError::from)?) |             let mut query = users::table | ||||||
|             .map_err(DbError::from)? |                 .select(User::as_select()) | ||||||
|             .into_iter() |                 .order(users::username.asc()) | ||||||
|             .map(gpodder::User::from) |                 .offset((page.page * page.per_page) as i64) | ||||||
|             .collect()) |                 .limit(page.per_page as i64) | ||||||
|  |                 .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) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ use axum::{ | ||||||
|     http::HeaderMap, |     http::HeaderMap, | ||||||
|     routing::{delete, get}, |     routing::{delete, get}, | ||||||
| }; | }; | ||||||
|  | use serde::Deserialize; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     server::{ |     server::{ | ||||||
|  | @ -22,22 +23,46 @@ pub fn router(ctx: Context) -> Router<Context> { | ||||||
|         )) |         )) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub async fn get_users( | #[derive(Deserialize, Clone)] | ||||||
|  | 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.admin(&session.user)?.paginated_users(page.into()) |         ctx.store | ||||||
|  |             .admin(&session.user)? | ||||||
|  |             .paginated_users(page.into(), &filter.into()) | ||||||
|     }) |     }) | ||||||
|     .await |     .await | ||||||
|     .unwrap()?; |     .unwrap()?; | ||||||
| 
 | 
 | ||||||
|     let next_page_query = |     let next_page_query = (users.len() == next_page.per_page as usize) | ||||||
|         (users.len() == next_page.per_page as usize).then_some(next_page.to_query()); |         .then_some(next_page.to_query().join(filter_clone)); | ||||||
| 
 | 
 | ||||||
|     Ok(View::Users(users, user_id, next_page_query) |     Ok(View::Users(users, user_id, next_page_query) | ||||||
|         .page(&headers) |         .page(&headers) | ||||||
|  |  | ||||||
|  | @ -23,6 +23,15 @@ 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
 | ||||||
|  |  | ||||||
|  | @ -1,15 +1,24 @@ | ||||||
| <h1>Users</h1> | <h1>Users</h1> | ||||||
| 
 | 
 | ||||||
| <table> | <input  | ||||||
|  |     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  | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue