-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new pages to list all keywords and search for a given keyword It is possible to go here using the "Keyword" button or using links in each crate's view
- Loading branch information
1 parent
4f94ae9
commit f3b4299
Showing
10 changed files
with
1,276 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
use std::num::NonZeroU32; | ||
use std::sync::Arc; | ||
|
||
use axum::extract::{Path, Query, State}; | ||
use axum::response::Redirect; | ||
use axum_extra::either::Either; | ||
use axum_extra::response::Html; | ||
use diesel::dsl as sql; | ||
use diesel::prelude::*; | ||
|
||
use json::json; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
use alexandrie_index::Indexer; | ||
|
||
use crate::config::AppState; | ||
use crate::db::models::Crate; | ||
use crate::db::schema::*; | ||
use crate::db::DATETIME_FORMAT; | ||
use crate::error::{Error, FrontendError}; | ||
use crate::frontend::helpers; | ||
use crate::utils::auth::frontend::Auth; | ||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||
pub(crate) struct QueryParams { | ||
pub page: Option<NonZeroU32>, | ||
} | ||
|
||
fn paginated_url(keyword: &str, page_number: u32, page_count: u32) -> Option<String> { | ||
if page_number >= 1 && page_number <= page_count { | ||
Some(format!("/keywords/{0}?page={1}", keyword, page_number)) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
pub(crate) async fn get( | ||
State(state): State<Arc<AppState>>, | ||
Query(params): Query<QueryParams>, | ||
Path(keyword): Path<String>, | ||
user: Option<Auth>, | ||
) -> Result<Either<Html<String>, Redirect>, FrontendError> { | ||
let page_number = params.page.map_or_else(|| 1, |page| page.get()); | ||
|
||
if state.is_login_required() && user.is_none() { | ||
return Ok(Either::E2(Redirect::to("/account/login"))); | ||
} | ||
|
||
let db = &state.db; | ||
let state = Arc::clone(&state); | ||
|
||
let transaction = db.transaction(move |conn| { | ||
|
||
//? Get the total count of search results. | ||
let total_results = crate_keywords::table | ||
.inner_join(keywords::table) | ||
.inner_join(crates::table) | ||
.filter(keywords::name.eq(&keyword)) | ||
.select(sql::count(crates::id)) | ||
.first::<i64>(conn)?; | ||
|
||
//? Get the search results for the given page number. | ||
//? First get all ids of crates with given keywords | ||
let results = crate_keywords::table | ||
.inner_join(keywords::table) | ||
.inner_join(crates::table) | ||
.filter(keywords::name.eq(&keyword)) | ||
.select(crates::id) | ||
.order_by(crates::downloads.desc()) | ||
.limit(15) | ||
.offset(15 * i64::from(page_number - 1)) | ||
.load::<i64>(conn)?; | ||
|
||
let results = results | ||
.into_iter() | ||
.map(|crate_ids| { | ||
let keywords = crate_keywords::table | ||
.inner_join(keywords::table) | ||
.select(keywords::name) | ||
.filter(crate_keywords::crate_id.eq(crate_ids)) | ||
.load::<String>(conn)?; | ||
|
||
let keyword_crate: Crate = crates::table | ||
.filter(crates::id.eq(crate_ids)) | ||
.first(conn)?; | ||
|
||
Ok((keyword_crate, keywords)) | ||
}) | ||
.collect::<Result<Vec<(Crate, Vec<String>)>, Error>>()?; | ||
|
||
//? Make page number starts counting from 1 (instead of 0). | ||
let page_count = (total_results / 15 | ||
+ if total_results > 0 && total_results % 15 == 0 { | ||
0 | ||
} else { | ||
1 | ||
}) as u32; | ||
|
||
let next_page = paginated_url(&keyword, page_number + 1, page_count); | ||
let prev_page = paginated_url(&keyword, page_number - 1, page_count); | ||
|
||
let auth = &state.frontend.config.auth; | ||
let engine = &state.frontend.handlebars; | ||
let context = json!({ | ||
"auth_disabled": !auth.enabled(), | ||
"registration_disabled": !auth.allow_registration(), | ||
"name": keyword, | ||
"user": user.map(|it| it.into_inner()), | ||
"instance": &state.frontend.config, | ||
"total_results": total_results, | ||
"pagination": { | ||
"current": page_number, | ||
"total_count": page_count, | ||
"next": next_page, | ||
"prev": prev_page, | ||
}, | ||
"results": results.into_iter().map(|(krate, keywords)| { | ||
let record = state.index.latest_record(&krate.name)?; | ||
let created_at = | ||
chrono::NaiveDateTime::parse_from_str(krate.created_at.as_str(), DATETIME_FORMAT) | ||
.unwrap(); | ||
let updated_at = | ||
chrono::NaiveDateTime::parse_from_str(krate.updated_at.as_str(), DATETIME_FORMAT) | ||
.unwrap(); | ||
Ok(json!({ | ||
"id": krate.id, | ||
"name": krate.name, | ||
"version": record.vers, | ||
"description": krate.description, | ||
"created_at": helpers::humanize_datetime(created_at), | ||
"updated_at": helpers::humanize_datetime(updated_at), | ||
"downloads": helpers::humanize_number(krate.downloads), | ||
"documentation": krate.documentation, | ||
"repository": krate.repository, | ||
"keywords": keywords, | ||
"yanked": record.yanked, | ||
})) | ||
}).collect::<Result<Vec<_>, Error>>()?, | ||
}); | ||
Ok(Either::E1(Html( | ||
engine.render("keywords", &context).unwrap(), | ||
))) | ||
}); | ||
|
||
transaction.await | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
use std::sync::Arc; | ||
|
||
use axum::extract::State; | ||
use axum::response::Redirect; | ||
use axum_extra::either::Either; | ||
use axum_extra::response::Html; | ||
use diesel::dsl as sql; | ||
use diesel::prelude::*; | ||
|
||
use json::json; | ||
|
||
use crate::config::AppState; | ||
use crate::db::models::Crate; | ||
use crate::db::schema::*; | ||
use crate::error::{Error, FrontendError}; | ||
use crate::utils::auth::frontend::Auth; | ||
|
||
pub(crate) async fn get( | ||
State(state): State<Arc<AppState>>, | ||
user: Option<Auth>, | ||
) -> Result<Either<Html<String>, Redirect>, FrontendError> { | ||
if state.is_login_required() && user.is_none() { | ||
return Ok(Either::E2(Redirect::to("/account/login"))); | ||
} | ||
|
||
let db = &state.db; | ||
let state = Arc::clone(&state); | ||
|
||
let transaction = db.transaction(move |conn| { | ||
//? Get the total count of search results. | ||
let total_results = keywords::table | ||
.select(sql::count(keywords::id)) | ||
.first::<i64>(conn)?; | ||
|
||
let keywords = keywords::table | ||
.select(keywords::name) | ||
.load::<String>(conn)?; | ||
|
||
let auth = &state.frontend.config.auth; | ||
let engine = &state.frontend.handlebars; | ||
let context = json!({ | ||
"auth_disabled": !auth.enabled(), | ||
"registration_disabled": !auth.allow_registration(), | ||
"user": user.map(|it| it.into_inner()), | ||
"instance": &state.frontend.config, | ||
"total_results": total_results, | ||
"keywords": keywords.into_iter().map(|keyword| { | ||
let crates = crate_keywords::table | ||
.inner_join(keywords::table) | ||
.inner_join(crates::table) | ||
.filter(keywords::name.eq(&keyword)) | ||
.order_by(crates::downloads.desc()) | ||
.select(crates::id) | ||
.limit(10) | ||
.load::<i64>(conn)? | ||
.into_iter() | ||
.map(|crate_ids| { | ||
let keyword_crate: Crate = crates::table | ||
.filter(crates::id.eq(crate_ids)) | ||
.first(conn)?; | ||
Ok(keyword_crate) | ||
}) | ||
.collect::<Result<Vec<_>, Error>>()?; | ||
let count = crate_keywords::table | ||
.inner_join(keywords::table) | ||
.inner_join(crates::table) | ||
.filter(keywords::name.eq(&keyword)) | ||
.select(sql::count(crates::id)) | ||
.first::<i64>(conn)?; | ||
Ok(json!({ | ||
"name":&keyword, | ||
"crates": crates, | ||
"count": count | ||
}))}).collect::<Result<Vec<_>, Error>>()?, | ||
}); | ||
Ok(Either::E1(Html( | ||
engine.render("keywords_index", &context).unwrap(), | ||
))) | ||
}); | ||
|
||
transaction.await | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
use std::num::NonZeroU32; | ||
use std::sync::Arc; | ||
|
||
use axum::extract::{Query, State}; | ||
use axum::response::Redirect; | ||
use axum_extra::either::Either; | ||
use axum_extra::response::Html; | ||
use diesel::dsl as sql; | ||
use diesel::prelude::*; | ||
|
||
use json::json; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
use crate::config::AppState; | ||
use crate::db::models::Keyword; | ||
use crate::db::schema::*; | ||
use crate::error::{Error, FrontendError}; | ||
use crate::utils::auth::frontend::Auth; | ||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||
pub(crate) struct QueryParams { | ||
pub q: String, | ||
pub page: Option<NonZeroU32>, | ||
} | ||
|
||
fn paginated_url(query: &str, page_number: u32, page_count: u32) -> Option<String> { | ||
let query = query.as_bytes(); | ||
let encoded_q = percent_encoding::percent_encode(query, percent_encoding::NON_ALPHANUMERIC); | ||
if page_number >= 1 && page_number <= page_count { | ||
Some(format!( | ||
"/keywords_search?q={0}&page={1}", | ||
encoded_q, page_number | ||
)) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
pub(crate) async fn get( | ||
State(state): State<Arc<AppState>>, | ||
Query(params): Query<QueryParams>, | ||
user: Option<Auth>, | ||
) -> Result<Either<Html<String>, Redirect>, FrontendError> { | ||
let page_number = params.page.map_or_else(|| 1, |page| page.get()); | ||
let searched_text = params.q.clone(); | ||
|
||
if state.is_login_required() && user.is_none() { | ||
return Ok(Either::E2(Redirect::to("/account/login"))); | ||
} | ||
|
||
let db = &state.db; | ||
let state = Arc::clone(&state); | ||
|
||
let transaction = db.transaction(move |conn| { | ||
let escaped_like_query = params.q.replace('\\', "\\\\").replace('%', "\\%"); | ||
let escaped_like_query = format!("%{escaped_like_query}%"); | ||
|
||
//? Get the total count of search results. | ||
let total_results = keywords::table | ||
.select(sql::count(keywords::id)) | ||
.filter(keywords::name.like(escaped_like_query.as_str())) | ||
.first::<i64>(conn)?; | ||
|
||
//? Get the search results for the given page number. | ||
let results: Vec<Keyword> = keywords::table | ||
.filter(keywords::name.like(escaped_like_query.as_str())) | ||
.limit(15) | ||
.offset(15 * i64::from(page_number - 1)) | ||
.load(conn)?; | ||
|
||
let results = results | ||
.into_iter() | ||
.map(|result| { | ||
let crates = crate_keywords::table | ||
.inner_join(crates::table) | ||
.select(crates::name) | ||
.filter(crate_keywords::keyword_id.eq(result.id)) | ||
.limit(5) | ||
.load::<String>(conn)?; | ||
Ok((result.name, crates)) | ||
}) | ||
.collect::<Result<Vec<(String, Vec<String>)>, Error>>()?; | ||
|
||
//? Make page number starts counting from 1 (instead of 0). | ||
let page_count = (total_results / 15 | ||
+ if total_results > 0 && total_results % 15 == 0 { | ||
0 | ||
} else { | ||
1 | ||
}) as u32; | ||
|
||
let next_page = paginated_url(¶ms.q, page_number + 1, page_count); | ||
let prev_page = paginated_url(¶ms.q, page_number - 1, page_count); | ||
|
||
let auth = &state.frontend.config.auth; | ||
let engine = &state.frontend.handlebars; | ||
let context = json!({ | ||
"auth_disabled": !auth.enabled(), | ||
"registration_disabled": !auth.allow_registration(), | ||
"user": user.map(|it| it.into_inner()), | ||
"instance": &state.frontend.config, | ||
"searched_text": searched_text, | ||
"total_results": total_results, | ||
"pagination": { | ||
"current": page_number, | ||
"total_count": page_count, | ||
"next": next_page, | ||
"prev": prev_page, | ||
}, | ||
"results": results.into_iter().map(|(keyword, crates)| { | ||
Ok(json!({ | ||
"name": keyword, | ||
"crates": crates, | ||
})) | ||
}).collect::<Result<Vec<_>, Error>>()?, | ||
}); | ||
Ok(Either::E1(Html( | ||
engine.render("keywords_search", &context).unwrap(), | ||
))) | ||
}); | ||
|
||
transaction.await | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.