diff --git a/crates/alexandrie/src/frontend/categories.rs b/crates/alexandrie/src/frontend/categories.rs new file mode 100644 index 00000000..c6750c16 --- /dev/null +++ b/crates/alexandrie/src/frontend/categories.rs @@ -0,0 +1,156 @@ +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, +} + +fn paginated_url(categorie: &str, page_number: u32, page_count: u32) -> Option { + if page_number >= 1 && page_number <= page_count { + Some(format!("/categories/{0}?page={1}", categorie, page_number)) + } else { + None + } +} + +pub(crate) async fn get( + State(state): State>, + Query(params): Query, + Path(categorie): Path, + user: Option, +) -> Result, 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_categories::table + .inner_join(categories::table) + .inner_join(crates::table) + .filter(categories::name.eq(&categorie)) + .select(sql::count(crates::id)) + .first::(conn)?; + + //? Get the search results for the given page number. + //? First get all ids of crates with given categories + let results = crate_categories::table + .inner_join(categories::table) + .inner_join(crates::table) + .filter(categories::name.eq(&categorie)) + .select(crates::id) + .order_by(crates::downloads.desc()) + .limit(15) + .offset(15 * i64::from(page_number - 1)) + .load::(conn)?; + + let results = results + .into_iter() + .map(|crate_ids| { + let categories = crate_categories::table + .inner_join(categories::table) + .select(categories::name) + .filter(crate_categories::crate_id.eq(crate_ids)) + .load::(conn)?; + + let categorie_crate: Crate = crates::table + .filter(crates::id.eq(crate_ids)) + .first(conn)?; + + Ok((categorie_crate, categories)) + }) + .collect::)>, Error>>()?; + + let description = categories::table + .filter(categories::name.eq(&categorie)) + .select(categories::description) + .first(conn); + + let description : String = match description { + Err(_e) => "".to_string(), + Ok(s) => s, + }; + + //? 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(&categorie, page_number + 1, page_count); + let prev_page = paginated_url(&categorie, 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": &categorie, + "description": description, + "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, categories)| { + 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, + "categories": categories, + "yanked": record.yanked, + })) + }).collect::, Error>>()?, + }); + Ok(Either::E1(Html( + engine.render("categories", &context).unwrap(), + ))) + }); + + transaction.await +} diff --git a/crates/alexandrie/src/frontend/categories_index.rs b/crates/alexandrie/src/frontend/categories_index.rs new file mode 100644 index 00000000..a069f245 --- /dev/null +++ b/crates/alexandrie/src/frontend/categories_index.rs @@ -0,0 +1,83 @@ +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>, + user: Option, +) -> Result, 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 = categories::table + .select(sql::count(categories::id)) + .first::(conn)?; + + let categories = categories::table + .select((categories::name, categories::description)) + .load::<(String, 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, + "categories": categories.into_iter().map(|categorie| { + let crates = crate_categories::table + .inner_join(categories::table) + .inner_join(crates::table) + .filter(categories::name.eq(&categorie.0)) + .order_by(crates::downloads.desc()) + .select(crates::id) + .limit(10) + .load::(conn)? + .into_iter() + .map(|crate_ids| { + let categorie_crate: Crate = crates::table + .filter(crates::id.eq(crate_ids)) + .first(conn)?; + Ok(categorie_crate) + }) + .collect::, Error>>()?; + let count = crate_categories::table + .inner_join(categories::table) + .inner_join(crates::table) + .filter(categories::name.eq(&categorie.0)) + .select(sql::count(crates::id)) + .first::(conn)?; + Ok(json!({ + "name":&categorie.0, + "description":&categorie.1, + "crates": crates, + "count": count + }))}).collect::, Error>>()?, + }); + Ok(Either::E1(Html( + engine.render("categories_index", &context).unwrap(), + ))) + }); + + transaction.await +} diff --git a/crates/alexandrie/src/frontend/mod.rs b/crates/alexandrie/src/frontend/mod.rs index a67998e0..91589d6c 100644 --- a/crates/alexandrie/src/frontend/mod.rs +++ b/crates/alexandrie/src/frontend/mod.rs @@ -15,6 +15,12 @@ pub mod most_downloaded; /// Search pages (eg. "/search?q=\"). pub mod search; +/// Categories page (eq. "/categories"). +pub mod categories_index; + +/// Categories page (eq. "/categories/\"). +pub mod categories; + /// Keywords index page (eq. "/keywords"). pub mod keywords_index; diff --git a/crates/alexandrie/src/main.rs b/crates/alexandrie/src/main.rs index 64c2e205..7c6e9841 100644 --- a/crates/alexandrie/src/main.rs +++ b/crates/alexandrie/src/main.rs @@ -96,6 +96,8 @@ fn frontend_routes(state: Arc, frontend_config: FrontendConfig) -> Rou .route("/most-downloaded", get(frontend::most_downloaded::get)) .route("/last-updated", get(frontend::last_updated::get)) .route("/crates/:crate", get(frontend::krate::get)) + .route("/categories", get(frontend::categories_index::get)) + .route("/categories/:categorie", get(frontend::categories::get)) .route("/keywords", get(frontend::keywords_index::get)) .route("/keywords/:keyword", get(frontend::keywords::get)) .route("/keywords_search", get(frontend::keywords_search::get)) diff --git a/templates/categories.hbs b/templates/categories.hbs new file mode 100644 index 00000000..09f8c721 --- /dev/null +++ b/templates/categories.hbs @@ -0,0 +1,313 @@ + + + + + {{> partials/head}} + + + + + {{> partials/navbar}} +
+
+
+
{{name}} category
+
{{description}}
+
+
+
+
+
+
{{ total_results }} total results
+
+
+
+ + + + diff --git a/templates/categories_index.hbs b/templates/categories_index.hbs new file mode 100644 index 00000000..6fa42267 --- /dev/null +++ b/templates/categories_index.hbs @@ -0,0 +1,252 @@ + + + + + {{> partials/head}} + + + + + {{> partials/navbar}} +
+
+
+
Categories
+
Categories and their most downloaded crates
+
+
+
+ {{#if categories}} + {{#each categories}} +
+ + + {{#if this.crates}} +
+ {{#each this.crates}}{{#if @index}}, {{/if}}{{ this.name }}{{/each}} +
+ {{else}} +
Empty list.
+ {{/if}} + +
+ {{/each}} + {{else}} +
+
No crates...
+
+ {{/if}} +
+ + + diff --git a/templates/crate.hbs b/templates/crate.hbs index 26594ac6..e6f6fb42 100644 --- a/templates/crate.hbs +++ b/templates/crate.hbs @@ -298,7 +298,7 @@
{{#if @first}}Part of{{else}}and{{/if}} 
-
{{ this }}
+ {{/each}} {{/if}} diff --git a/templates/partials/navbar.hbs b/templates/partials/navbar.hbs index 660cce9a..ac0c21dd 100644 --- a/templates/partials/navbar.hbs +++ b/templates/partials/navbar.hbs @@ -294,6 +294,9 @@ {{ instance.title }} + + Categories + Keywords @@ -333,6 +336,9 @@ {{ instance.title }} + + Categories + Keywords