From dae6a84de4fed35acab26e7ed6a84dc3e356708a Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 11 Oct 2021 19:18:31 +0100 Subject: [PATCH] Show 'most downloaded' on homepage --- chartered-db/src/crates.rs | 40 ++++++++++++++++++++++++++++++++++++++++ chartered-db/src/schema.rs | 1 + migrations/2021-08-31-214501_create_crates_table/up.sql | 1 + chartered-frontend/src/pages/Dashboard.tsx | 43 +++++++++++++++++++++++++++++++++++++------ chartered-web/src/endpoints/cargo_api/download.rs | 8 ++++++++ chartered-web/src/endpoints/web_api/crates/mod.rs | 2 ++ chartered-web/src/endpoints/web_api/crates/most_downloaded.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 14 deletions(-) diff --git a/chartered-db/src/crates.rs b/chartered-db/src/crates.rs index b5678c4..671fe62 100644 --- a/chartered-db/src/crates.rs +++ a/chartered-db/src/crates.rs @@ -53,6 +53,7 @@ pub repository: Option, pub homepage: Option, pub documentation: Option, + pub downloads: i32, } macro_rules! crate_with_permissions { @@ -163,6 +164,31 @@ .await? } + pub async fn list_most_downloaded( + conn: ConnectionPool, + requesting_user_id: i32, + ) -> Result> { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let crates = crate_with_permissions!(requesting_user_id) + .filter( + select_permissions!() + .bitwise_and(UserPermission::VISIBLE.bits()) + .eq(UserPermission::VISIBLE.bits()), + ) + .inner_join(organisations::table) + .inner_join(crate_versions::table) + .select((crates::all_columns, organisations::all_columns)) + .limit(10) + .order_by(crate::schema::crates::dsl::downloads.desc()) + .load(&conn)?; + + Ok(crates) + }) + .await? + } + pub async fn list_recently_updated( conn: ConnectionPool, requesting_user_id: i32, @@ -326,6 +352,20 @@ Ok(CrateVersion::belonging_to(&self.crate_) .inner_join(users::table) .load::<(CrateVersion, User)>(&conn)?) + }) + .await? + } + + pub async fn increment_download_count(self: Arc, conn: ConnectionPool) -> Result<()> { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let _ = diesel::update(crates::table) + .set(crates::downloads.eq(crates::downloads + 1)) + .filter(crates::id.eq(self.crate_.id)) + .execute(&conn)?; + + Ok(()) }) .await? } diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs index 6010788..74790d4 100644 --- a/chartered-db/src/schema.rs +++ a/chartered-db/src/schema.rs @@ -25,6 +25,7 @@ repository -> Nullable, homepage -> Nullable, documentation -> Nullable, + downloads -> Integer, } } diff --git a/migrations/2021-08-31-214501_create_crates_table/up.sql b/migrations/2021-08-31-214501_create_crates_table/up.sql index 3c136d1..4d9cd99 100644 --- a/migrations/2021-08-31-214501_create_crates_table/up.sql +++ a/migrations/2021-08-31-214501_create_crates_table/up.sql @@ -30,6 +30,7 @@ repository VARCHAR(255), homepage VARCHAR(255), documentation VARCHAR(255), + downloads INTEGER NOT NULL DEFAULT 0, UNIQUE (name, organisation_id), FOREIGN KEY (organisation_id) REFERENCES organisations (id) ); diff --git a/chartered-frontend/src/pages/Dashboard.tsx b/chartered-frontend/src/pages/Dashboard.tsx index 0db2797..a7b606e 100644 --- a/chartered-frontend/src/pages/Dashboard.tsx +++ a/chartered-frontend/src/pages/Dashboard.tsx @@ -1,9 +1,9 @@ import React = require("react"); import { Link } from "react-router-dom"; import { useAuth } from "../useAuth"; import Nav from "../sections/Nav"; -import { ChevronRight } from "react-bootstrap-icons"; +import {ChevronRight, Download} from "react-bootstrap-icons"; import { useAuthenticatedRequest } from "../util"; interface RecentlyUpdatedResponse { @@ -16,15 +16,31 @@ organisation: string; } +interface MostDownloadedResponse { + crates: MostDownloadedResponseCrate[]; +} + +interface MostDownloadedResponseCrate { + name: string; + downloads: number; + organisation: string; +} + export default function Dashboard() { const auth = useAuth(); - const { response: recentlyUpdated, error } = + const { response: recentlyUpdated, error: recentlyUpdatedError } = useAuthenticatedRequest({ auth, endpoint: "crates/recently-updated", }); + const { response: mostDownloaded, error: mostDownloadedError } = + useAuthenticatedRequest({ + auth, + endpoint: "crates/most-downloaded", + }); + return (
); -} - -interface Crate { - name: string; - version: string; - organisation: string; } -function CrateCard({ crate }: { crate: Crate }) { +function CrateCard({ name, organisation, children }: React.PropsWithChildren<{ name: string, organisation: string }>) { return (
- {crate.organisation}/ - {crate.name} + {organisation}/ + {name}
- v{crate.version} + {children}
diff --git a/chartered-web/src/endpoints/cargo_api/download.rs b/chartered-web/src/endpoints/cargo_api/download.rs index d9138e1..b26c2d7 100644 --- a/chartered-web/src/endpoints/cargo_api/download.rs +++ a/chartered-web/src/endpoints/cargo_api/download.rs @@ -41,6 +41,14 @@ let crate_with_permissions = Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); + // we shouldn't really hold back this request from progressing whilst waiting + // on the downloads increment to complete so we'll just tokio::spawn it + tokio::spawn( + crate_with_permissions + .clone() + .increment_download_count(db.clone()), + ); + let version = crate_with_permissions .version(db, version) .await? diff --git a/chartered-web/src/endpoints/web_api/crates/mod.rs b/chartered-web/src/endpoints/web_api/crates/mod.rs index 28f6bd4..e2e3d49 100644 --- a/chartered-web/src/endpoints/web_api/crates/mod.rs +++ a/chartered-web/src/endpoints/web_api/crates/mod.rs @@ -1,5 +1,6 @@ mod info; mod members; +mod most_downloaded; mod recently_updated; mod search; @@ -28,5 +29,6 @@ .route("/:org/:crate/members", put(members::handle_put)) .route("/:org/:crate/members", delete(members::handle_delete)) .route("/recently-updated", get(recently_updated::handle)) + .route("/most-downloaded", get(most_downloaded::handle)) .route("/search", get(search::handle))) } diff --git a/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs b/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs new file mode 100644 index 0000000..cabad18 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs @@ -1,0 +1,51 @@ +use axum::{extract, Json}; +use chartered_db::{crates::Crate, users::User, ConnectionPool}; +use serde::Serialize; +use std::sync::Arc; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } +} + +define_error_response!(Error); + +pub async fn handle( + extract::Extension(db): extract::Extension, + extract::Extension(user): extract::Extension>, +) -> Result, Error> { + let crates_with_versions = Crate::list_most_downloaded(db, user.id).await?; + + Ok(Json(Response { + crates: crates_with_versions + .into_iter() + .map(|(crate_, organisation)| ResponseCrate { + name: crate_.name, + downloads: crate_.downloads, + organisation: organisation.name, + }) + .collect(), + })) +} + +#[derive(Serialize)] +pub struct Response { + crates: Vec, +} + +#[derive(Serialize)] +pub struct ResponseCrate { + name: String, + downloads: i32, + organisation: String, +} -- rgit 0.1.3