Skip to content

Commit 07abaf1

Browse files
committed
feat: [torrust#801] added filtering for both drivers
1 parent ddde58f commit 07abaf1

File tree

4 files changed

+185
-12
lines changed

4 files changed

+185
-12
lines changed

src/databases/database.rs

+11
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ pub enum UsersSorting {
8282
UsernameZA,
8383
}
8484

85+
/// Sorting options for users.
86+
#[derive(Clone, Copy, Debug, Deserialize)]
87+
pub enum UsersFilters {
88+
EmailVerified,
89+
EmailNotVerified,
90+
TorrentUploader,
91+
}
92+
8593
/// Database errors.
8694
#[derive(Debug)]
8795
pub enum Error {
@@ -165,6 +173,9 @@ pub trait Database: Sync + Send {
165173
/// Get `UserCompact` from `user_id`.
166174
async fn get_user_compact_from_id(&self, user_id: i64) -> Result<UserCompact, Error>;
167175

176+
/// Get `UsersFilter` from `filter_name`.
177+
async fn get_filters_from_name(&self, filter_name: &str) -> Option<UsersFilters>;
178+
168179
/// Get a user's `TrackerKey`.
169180
async fn get_user_tracker_key(&self, user_id: i64) -> Option<TrackerKey>;
170181

src/databases/mysql.rs

+67-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions};
88
use sqlx::{query, query_as, Acquire, ConnectOptions, MySqlPool};
99
use url::Url;
1010

11-
use super::database::{UsersSorting, TABLES_TO_TRUNCATE};
11+
use super::database::{UsersFilters, UsersSorting, TABLES_TO_TRUNCATE};
1212
use crate::databases::database;
1313
use crate::databases::database::{Category, Database, Driver, Sorting, TorrentCompact};
1414
use crate::models::category::CategoryId;
@@ -175,18 +175,71 @@ impl Database for Mysql {
175175
UsersSorting::UsernameZA => "username DESC".to_string(),
176176
};
177177

178-
let mut query_string = "SELECT
178+
let join_filters_query = if let Some(filters) = filters {
179+
let mut join_filters = String::new();
180+
for filter in filters {
181+
// don't take user input in the db query to smt join filter query
182+
if let Some(sanitized_filter) = self.get_filters_from_name(filter).await {
183+
match sanitized_filter {
184+
UsersFilters::TorrentUploader => join_filters.push_str(
185+
"INNER JOIN torrust_torrents tt
186+
ON tu.user_id = tt_uploader_id",
187+
),
188+
_ => break,
189+
}
190+
}
191+
}
192+
join_filters
193+
} else {
194+
String::new()
195+
};
196+
197+
let where_filters_query = if let Some(filters) = filters {
198+
let mut i = 0;
199+
let mut where_filters = String::new();
200+
for filter in filters {
201+
// don't take user input in the db query
202+
if let Some(sanitized_filter) = self.get_filters_from_name(filter).await {
203+
let mut filter_query = String::new();
204+
match sanitized_filter {
205+
UsersFilters::EmailNotVerified => filter_query.push_str("email_verified = false"),
206+
UsersFilters::EmailVerified => filter_query.push_str("email_verified = true"),
207+
_ => break,
208+
};
209+
210+
let mut str = format!("'{}'", filter_query);
211+
if i > 0 {
212+
str = format!(" AND {str}");
213+
}
214+
where_filters.push_str(&str);
215+
i += 1;
216+
}
217+
}
218+
if where_filters.is_empty() {
219+
String::new()
220+
} else {
221+
String::new()
222+
}
223+
} else {
224+
String::new()
225+
};
226+
227+
let mut query_string = format!(
228+
"SELECT
179229
tp.user_id,
180230
tp.username,
181231
tp.email,
182232
tp.email_verified,
183233
tu.date_registered,
184234
tu.administrator
185235
FROM torrust_user_profiles tp
186-
INNER JOIN torrust_users tu
236+
INNER JOIN torrust_users tu
187237
ON tp.user_id = tu.user_id
188-
WHERE username LIKE ?"
189-
.to_string();
238+
{join_filters_query}
239+
WHERE username LIKE ?
240+
{where_filters_query}
241+
"
242+
);
190243

191244
let count_query = format!("SELECT COUNT(*) as count FROM ({query_string}) AS count_table");
192245

@@ -223,6 +276,15 @@ impl Database for Mysql {
223276
.map_err(|_| database::Error::UserNotFound)
224277
}
225278

279+
async fn get_filters_from_name(&self, filter_name: &str) -> Option<UsersFilters> {
280+
match filter_name {
281+
"torrentUploader" => Some(UsersFilters::TorrentUploader),
282+
"emailNotVerified" => Some(UsersFilters::EmailNotVerified),
283+
"emailVerified" => Some(UsersFilters::EmailVerified),
284+
_ => None,
285+
}
286+
}
287+
226288
/// Gets User Tracker Key
227289
///
228290
/// # Panics

src/databases/sqlite.rs

+85-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
88
use sqlx::{query, query_as, Acquire, ConnectOptions, SqlitePool};
99
use url::Url;
1010

11-
use super::database::TABLES_TO_TRUNCATE;
11+
use super::database::{UsersFilters, UsersSorting, TABLES_TO_TRUNCATE};
1212
use crate::databases::database;
1313
use crate::databases::database::{Category, Database, Driver, Sorting, TorrentCompact};
1414
use crate::models::category::CategoryId;
@@ -159,6 +159,8 @@ impl Database for Sqlite {
159159
async fn get_user_profiles_search_paginated(
160160
&self,
161161
search: &Option<String>,
162+
filters: &Option<Vec<String>>,
163+
sort: &UsersSorting,
162164
offset: u64,
163165
limit: u8,
164166
) -> Result<UserProfilesResponse, database::Error> {
@@ -167,7 +169,78 @@ impl Database for Sqlite {
167169
Some(v) => format!("%{v}%"),
168170
};
169171

170-
let mut query_string = "SELECT * FROM torrust_user_profiles WHERE username LIKE ?".to_string();
172+
let sort_query: String = match sort {
173+
UsersSorting::DateRegisteredNewest => "date_registered ASC".to_string(),
174+
UsersSorting::DateRegisteredOldest => "date_registered DESC".to_string(),
175+
UsersSorting::UsernameAZ => "username ASC".to_string(),
176+
UsersSorting::UsernameZA => "username DESC".to_string(),
177+
};
178+
179+
let join_filters_query = if let Some(filters) = filters {
180+
let mut join_filters = String::new();
181+
for filter in filters {
182+
// don't take user input in the db query
183+
if let Some(sanitized_filter) = self.get_filters_from_name(filter).await {
184+
match sanitized_filter {
185+
UsersFilters::TorrentUploader => join_filters.push_str(
186+
"INNER JOIN torrust_torrents tt
187+
ON tu.user_id = tt_uploader_id",
188+
),
189+
_ => break,
190+
}
191+
}
192+
}
193+
join_filters
194+
} else {
195+
String::new()
196+
};
197+
198+
let where_filters_query = if let Some(filters) = filters {
199+
let mut i = 0;
200+
let mut where_filters = String::new();
201+
for filter in filters {
202+
// don't take user input in the db query
203+
if let Some(sanitized_filter) = self.get_filters_from_name(filter).await {
204+
let mut filter_query = String::new();
205+
match sanitized_filter {
206+
UsersFilters::EmailNotVerified => filter_query.push_str("email_verified = false"),
207+
UsersFilters::EmailVerified => filter_query.push_str("email_verified = true"),
208+
_ => break,
209+
};
210+
211+
let mut str = format!("'{}'", filter_query);
212+
if i > 0 {
213+
str = format!(" AND {str}");
214+
}
215+
where_filters.push_str(&str);
216+
i += 1;
217+
}
218+
}
219+
if where_filters.is_empty() {
220+
String::new()
221+
} else {
222+
String::new()
223+
}
224+
} else {
225+
String::new()
226+
};
227+
228+
let mut query_string = format!(
229+
"SELECT
230+
tp.user_id,
231+
tp.username,
232+
tp.email,
233+
tp.email_verified,
234+
tu.date_registered,
235+
tu.administrator
236+
FROM torrust_user_profiles tp
237+
INNER JOIN torrust_users tu
238+
ON tp.user_id = tu.user_id
239+
{join_filters_query}
240+
WHERE username LIKE ?
241+
{where_filters_query}
242+
"
243+
);
171244

172245
let count_query = format!("SELECT COUNT(*) as count FROM ({query_string}) AS count_table");
173246

@@ -180,7 +253,7 @@ impl Database for Sqlite {
180253

181254
let count = count_result?;
182255

183-
query_string = format!("{query_string} LIMIT ?, ?");
256+
query_string = format!("{query_string} ORDER BY {sort_query} LIMIT ?, ?");
184257

185258
let res: Vec<UserProfile> = sqlx::query_as::<_, UserProfile>(&query_string)
186259
.bind(user_name.clone())
@@ -204,6 +277,15 @@ impl Database for Sqlite {
204277
.map_err(|_| database::Error::UserNotFound)
205278
}
206279

280+
async fn get_filters_from_name(&self, filter_name: &str) -> Option<UsersFilters> {
281+
match filter_name {
282+
"torrentUploader" => Some(UsersFilters::TorrentUploader),
283+
"emailNotVerified" => Some(UsersFilters::EmailNotVerified),
284+
"emailVerified" => Some(UsersFilters::EmailVerified),
285+
_ => None,
286+
}
287+
}
288+
207289
async fn get_user_tracker_key(&self, user_id: i64) -> Option<TrackerKey> {
208290
const HOUR_IN_SECONDS: i64 = 3600;
209291

src/services/user.rs

+22-4
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ use tracing::{debug, info};
1414
use super::authentication::DbUserAuthenticationRepository;
1515
use super::authorization::{self, ACTION};
1616
use crate::config::{Configuration, PasswordConstraints};
17-
use crate::databases::database::{Database, Error};
17+
use crate::databases::database::{Database, Error, UsersSorting};
1818
use crate::errors::ServiceError;
19-
use crate::mailer;
2019
use crate::mailer::VerifyClaims;
2120
use crate::models::response::UserProfilesResponse;
2221
use crate::models::user::{UserCompact, UserId, UserProfile, Username};
2322
use crate::services::authentication::verify_password;
2423
use crate::utils::validation::validate_email_address;
2524
use crate::web::api::server::v1::contexts::user::forms::{ChangePasswordForm, RegistrationForm};
25+
use crate::{mailer, AsCSV};
2626

2727
/// Since user email could be optional, we need a way to represent "no email"
2828
/// in the database. This function returns the string that should be used for
@@ -34,6 +34,9 @@ fn no_email() -> String {
3434
/// User request to generate a user profile listing.
3535
#[derive(Debug, Deserialize)]
3636
pub struct ListingRequest {
37+
/// Expects comma separated string
38+
pub filters: Option<String>,
39+
pub sort: Option<UsersSorting>,
3740
pub page_size: Option<u8>,
3841
pub page: Option<u32>,
3942
pub search: Option<String>,
@@ -43,6 +46,9 @@ pub struct ListingRequest {
4346
#[derive(Debug, Deserialize)]
4447
pub struct ListingSpecification {
4548
pub offset: u64,
49+
/// Expects comma separated string
50+
pub filters: Option<Vec<String>>,
51+
pub sort: UsersSorting,
4652
pub page_size: u8,
4753
pub search: Option<String>,
4854
}
@@ -403,10 +409,16 @@ impl ListingService {
403409

404410
let offset = u64::from(page * u32::from(page_size));
405411

412+
let sort = request.sort.unwrap_or(UsersSorting::UsernameAZ);
413+
414+
let filters = request.filters.as_csv::<String>().unwrap_or(None);
415+
406416
ListingSpecification {
407-
search: request.search.clone(),
408417
offset,
418+
filters,
419+
sort,
409420
page_size,
421+
search: request.search.clone(),
410422
}
411423
}
412424
}
@@ -510,7 +522,13 @@ impl DbUserProfileRepository {
510522
/// It returns an error if there is a database error.
511523
pub async fn generate_listing(&self, specification: &ListingSpecification) -> Result<UserProfilesResponse, Error> {
512524
self.database
513-
.get_user_profiles_search_paginated(&specification.search, specification.offset, specification.page_size)
525+
.get_user_profiles_search_paginated(
526+
&specification.search,
527+
&specification.filters,
528+
&specification.sort,
529+
specification.offset,
530+
specification.page_size,
531+
)
514532
.await
515533
}
516534
}

0 commit comments

Comments
 (0)