Compare commits
	
		
			2 Commits 
		
	
	
		
			c7c5cf889c
			...
			6c8183c1e3
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 6c8183c1e3 | |
|  | fce301080c | 
|  | @ -72,3 +72,8 @@ pub struct Page { | |||
|     pub 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> { | ||||
|     pub fn paginated_users(&self, page: Page) -> Result<Vec<models::User>, AuthErr> { | ||||
|         self.store.paginated_users(page) | ||||
|     pub fn paginated_users( | ||||
|         &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>; | ||||
| 
 | ||||
|     /// 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 { | ||||
|  |  | |||
|  | @ -143,16 +143,31 @@ impl gpodder::GpodderAuthStore for SqliteRepository { | |||
|         .map_err(AuthErr::from) | ||||
|     } | ||||
| 
 | ||||
|     fn paginated_users(&self, page: gpodder::Page) -> Result<Vec<gpodder::User>, AuthErr> { | ||||
|         Ok(users::table | ||||
|             .select(User::as_select()) | ||||
|             .order(users::username.asc()) | ||||
|             .offset((page.page * page.per_page) as i64) | ||||
|             .limit(page.per_page as i64) | ||||
|             .get_results(&mut self.pool.get().map_err(DbError::from)?) | ||||
|             .map_err(DbError::from)? | ||||
|             .into_iter() | ||||
|             .map(gpodder::User::from) | ||||
|             .collect()) | ||||
|     fn paginated_users( | ||||
|         &self, | ||||
|         page: gpodder::Page, | ||||
|         filter: &gpodder::UserFilter, | ||||
|     ) -> Result<Vec<gpodder::User>, AuthErr> { | ||||
|         (|| { | ||||
|             let mut query = users::table | ||||
|                 .select(User::as_select()) | ||||
|                 .order(users::username.asc()) | ||||
|                 .offset((page.page * page.per_page) as i64) | ||||
|                 .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, | ||||
|     routing::{delete, get}, | ||||
| }; | ||||
| use serde::Deserialize; | ||||
| 
 | ||||
| use crate::{ | ||||
|     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>, | ||||
|     headers: HeaderMap, | ||||
|     Extension(session): Extension<gpodder::Session>, | ||||
|     Query(page): Query<super::Pagination>, | ||||
|     Query(filter): Query<UserFilter>, | ||||
| ) -> AppResult<TemplateResponse<Page<View>>> { | ||||
|     let next_page = page.next_page(); | ||||
|     let filter_clone = filter.clone(); | ||||
| 
 | ||||
|     let user_id = session.user.id; | ||||
|     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 | ||||
|     .unwrap()?; | ||||
| 
 | ||||
|     let next_page_query = | ||||
|         (users.len() == next_page.per_page as usize).then_some(next_page.to_query()); | ||||
|     let next_page_query = (users.len() == next_page.per_page as usize) | ||||
|         .then_some(next_page.to_query().join(filter_clone)); | ||||
| 
 | ||||
|     Ok(View::Users(users, user_id, next_page_query) | ||||
|         .page(&headers) | ||||
|  |  | |||
|  | @ -23,6 +23,15 @@ impl Query { | |||
| 
 | ||||
|         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
 | ||||
|  |  | |||
|  | @ -1,15 +1,24 @@ | |||
| <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> | ||||
|         <th>Username</th> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|         {% for user in users %} | ||||
|         {%- for user in users %} | ||||
|         <tr> | ||||
|             <th>{{ user.username }}</th> | ||||
|         </tr> | ||||
|         {% endfor %} | ||||
|         {% endfor -%} | ||||
| 
 | ||||
|         {%- if next_page_query %} | ||||
|         <tr  | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue