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