diff options
Diffstat (limited to 'front_end/src/author_page.rs')
-rw-r--r-- | front_end/src/author_page.rs | 259 |
1 files changed, 144 insertions, 115 deletions
diff --git a/front_end/src/author_page.rs b/front_end/src/author_page.rs index 5bb2ae2..52fd568 100644 --- a/front_end/src/author_page.rs +++ b/front_end/src/author_page.rs @@ -1,62 +1,174 @@ +use chrono::prelude::*; use crate::Page; use crate::templates; +use crate::url_domain; use futures::stream::StreamExt; use kitchen_sink::CrateOwnerRow; use kitchen_sink::CResult; use kitchen_sink::KitchenSink; +use kitchen_sink::Org; +use kitchen_sink::OwnerKind; use kitchen_sink::RichAuthor; use kitchen_sink::RichCrateVersion; -use kitchen_sink::UserOrg; +use kitchen_sink::User; use kitchen_sink::UserType; use render_readme::Renderer; +use std::borrow::Cow; +use std::cmp::Ordering; +use std::collections::HashMap; use std::sync::Arc; -// pub struct User { -// pub id: u32, -// pub login: String, -// pub name: Option<String>, -// pub avatar_url: Option<String>, // "https://avatars0.githubusercontent.com/u/1111?v=4", -// pub gravatar_id: Option<String>, // "", -// pub html_url: String, // "https://github.com/zzzz", -// pub blog: Option<String>, // "https://example.com -// #[serde(rename = "type")] -// pub user_type: UserType, -// } +pub struct OtherOwner { + github_id: u32, + login: String, + invited_by_github_id: Option<u32>, + invited_at: DateTime<Utc>, + kind: OwnerKind, +} /// Data sources used in `author.rs.html` pub struct AuthorPage<'a> { - pub aut: &'a RichAuthor, - pub kitchen_sink: &'a KitchenSink, - pub markup: &'a Renderer, - pub crates: Vec<(Arc<RichCrateVersion>, CrateOwnerRow)>, - pub orgs: Vec<UserOrg>, + pub(crate) aut: &'a RichAuthor, + pub(crate) markup: &'a Renderer, + pub(crate) founder_crates: Vec<(Arc<RichCrateVersion>, u32, CrateOwnerRow, Vec<OtherOwner>)>, + pub(crate) member_crates: Vec<(Arc<RichCrateVersion>, u32, CrateOwnerRow, Vec<OtherOwner>)>, + pub(crate) orgs: Vec<Org>, + pub(crate) joined: DateTime<Utc>, + pub(crate) founder_total: usize, + pub(crate) member_total: usize, + pub(crate) keywords: Vec<String>, + pub(crate) collab: Vec<User>, } impl<'a> AuthorPage<'a> { pub async fn new(aut: &'a RichAuthor, kitchen_sink: &'a KitchenSink, markup: &'a Renderer) -> CResult<AuthorPage<'a>> { dbg!(&aut); let orgs = kitchen_sink.user_github_orgs(&aut.github.login).await?.unwrap_or_default(); - let mut rows = kitchen_sink.crates_of_author(aut).await?; - rows.sort_by(|a,b| b.latest_release.cmp(&a.latest_release)); - rows.truncate(200); - dbg!(&rows); + let orgs = futures::stream::iter(orgs).filter_map(|org| async move { + kitchen_sink.github_org(&org.login).await + .map_err(|e| eprintln!("org: {} {}", &org.login, e)) + .ok().and_then(|x| x) + }) + .collect().await; + let rows = kitchen_sink.crates_of_author(aut).await?; + let joined = rows.iter().filter_map(|row| row.invited_at).min().expect("no crates?"); + + let (mut founder, mut member): (Vec<_>, Vec<_>) = rows.into_iter().partition(|c| c.invited_by_github_id.is_none()); + let founder_total = founder.len(); + let member_total = member.len(); + founder.sort_by(|a,b| b.latest_release.cmp(&a.latest_release)); + founder.truncate(200); + + member.sort_by(|a,b| b.crate_ranking.partial_cmp(&a.crate_ranking).unwrap_or(Ordering::Equal)); + member.truncate(200); + + let founder_crates = Self::look_up(kitchen_sink, founder).await; + let member_crates = Self::look_up(kitchen_sink, member).await; + + // Most common keywords + let mut keywords = HashMap::new(); + let now = Utc::now(); + // Most collaborated with + let mut collab = HashMap::new(); + for (c, _, row, all_owners) in founder_crates.iter().chain(member_crates.iter()) { + for k in c.keywords() { + *keywords.entry(k).or_insert(0.) += row.crate_ranking + 0.5; + } + + if let Some(own) = all_owners.iter().find(|o| o.github_id == aut.github.id) { + let oldest = all_owners.iter().map(|o| o.invited_at).min().unwrap(); + // max discounts young crates + let max_days = now.signed_duration_since(oldest).num_days().max(30*6) as f32; + let own_days = now.signed_duration_since(own.invited_at).num_days(); + for o in all_owners { + if o.github_id == aut.github.id || o.kind != OwnerKind::User { + continue; + } + // How long co-owned together, relative to crate's age + let overlap = now.signed_duration_since(o.invited_at).num_days().min(own_days) as f32 / max_days; + let relationship = if own.invited_by_github_id == Some(o.github_id) {4.} + else if o.invited_by_github_id == Some(own.github_id) {2.} else {1.}; + collab.entry(o.github_id).or_insert((0., &o.login)).0 += (row.crate_ranking + 0.5) * overlap * relationship; + } + } + } + let mut keywords: Vec<_> = keywords.into_iter().collect(); + keywords.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)); + let keywords: Vec<_> = keywords.into_iter().take(7).map(|(k, _)| k.to_owned()).collect(); - let crates = futures::stream::iter(rows.into_iter()) - .filter_map(|row| async move { - let c = kitchen_sink.rich_crate_version_async(&row.origin).await.map_err(|e| eprintln!("{}", e)).ok()?; - Some((c, row)) - }) - .collect().await; + let mut collab: Vec<_> = collab.into_iter().map(|(_, v)| v).collect(); + collab.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal)); + let collab: Vec<_> = futures::stream::iter(collab.into_iter().take(100)).filter_map(|(_, login)| async move { + kitchen_sink.user_by_github_login(login).await.map_err(|e| eprintln!("{}: {}", login, e)).ok().and_then(|x| x) + }).collect().await; Ok(Self { - crates, + founder_crates, member_crates, + founder_total, member_total, aut, - kitchen_sink, markup, orgs, + joined, + keywords, + collab, }) } + async fn look_up(kitchen_sink: &KitchenSink, rows: Vec<CrateOwnerRow>) -> Vec<(Arc<RichCrateVersion>, u32, CrateOwnerRow, Vec<OtherOwner>)> { + futures::stream::iter(rows.into_iter()) + .filter_map(|row| async move { + let c = kitchen_sink.rich_crate_version_async(&row.origin).await.map_err(|e| eprintln!("{}", e)).ok()?; + let dl = kitchen_sink.downloads_per_month(&row.origin).await.map_err(|e| eprintln!("{}", e)).ok()?.unwrap_or(0) as u32; + let owners = kitchen_sink.crate_owners(&row.origin).await.map_err(|e| eprintln!("o: {}", e)).ok()?.into_iter().filter_map(|o| { + Some(OtherOwner { + invited_at: o.invited_at()?, + github_id: o.github_id?, + invited_by_github_id: o.invited_by_github_id, + login: o.login, + kind: o.kind, + }) + }).collect(); + Some((c, dl, row, owners)) + }) + .collect().await + } + + /// `(url, label)` + pub fn homepage_link(&self) -> Option<(&str, Cow<'_, str>)> { + if let Some(url) = self.aut.github.blog.as_deref() { + if url.starts_with("https://") || url.starts_with("http://") { + let label = url_domain(url) + .map(|host| { + format!("Home ({})", host).into() + }) + .unwrap_or_else(|| "Homepage".into()); + return Some((url, label)); + } + } + None + } + + pub fn joined_github(&self) -> Option<DateTime<FixedOffset>> { + if let Some(d) = &self.aut.github.created_at { + DateTime::parse_from_rfc3339(d).ok() + } else { + None + } + } + + pub fn org_name(org: &Org) -> &str { + if let Some(name) = &org.name { + if name.eq_ignore_ascii_case(&org.login) { + return &name; + } + } + &org.login + } + + pub fn format_month(date: &DateTime<Utc>) -> String { + date.format("%b %Y").to_string() + } + pub fn is_org(&self) -> bool { self.aut.github.user_type == UserType::Org } @@ -71,7 +183,9 @@ impl<'a> AuthorPage<'a> { pub fn page(&self) -> Page { Page { - title: format!("Rust crates by @{}", self.login()), + title: format!("@{}'s Rust crates", self.login()), + critical_css_data: Some(include_str!("../../style/public/author.css")), + critical_css_dev_url: Some("/author.css"), ..Default::default() } } @@ -79,89 +193,4 @@ impl<'a> AuthorPage<'a> { pub fn render_markdown_str(&self, s: &str) -> templates::Html<String> { templates::Html(self.markup.markdown_str(s, true, None)) } - - // fn block<O>(&self, f: impl Future<Output = O>) -> O { - // self.handle.enter(|| futures::executor::block_on(f)) - // } - - // pub fn format_number(&self, num: impl Display) -> String { - // Numeric::english().format_int(num) - // } - - // pub fn format_knumber(&self, num: usize) -> (String, &'static str) { - // let (num, unit) = match num { - // 0..=899 => (num, ""), - // 0..=8000 => return (format!("{}", ((num + 250) / 500) as f64 * 0.5), "K"), // 3.5K - // 0..=899_999 => ((num + 500) / 1000, "K"), - // 0..=9_999_999 => return (format!("{}", ((num + 250_000) / 500_000) as f64 * 0.5), "M"), // 3.5M - // _ => ((num + 500_000) / 1_000_000, "M"), // 10M - // }; - // (Numeric::english().format_int(num), unit) - // } - - // pub fn format_kbytes(&self, bytes: usize) -> String { - // let (num, unit) = match bytes { - // 0..=100_000 => ((bytes + 999) / 1000, "KB"), - // 0..=800_000 => ((bytes + 3999) / 5000 * 5, "KB"), - // 0..=9_999_999 => return format!("{}MB", ((bytes + 250_000) / 500_000) as f64 * 0.5), - // _ => ((bytes + 500_000) / 1_000_000, "MB"), - // }; - // format!("{}{}", Numeric::english().format_int(num), unit) - // } - - // fn format_number_frac(num: f64) -> String { - // if num > 0.05 && num < 10. && num.fract() > 0.09 && num.fract() < 0.9 { - // if num < 3. { - // format!("{:.1}", num) - // } else { - // format!("{}", (num * 2.).round() / 2.) - // } - // } else { - // Numeric::english().format_int(if num > 500. { - // (num / 10.).round() * 10. - // } else if num > 100. { - // (num / 5.).round() * 5. - // } else { - // num.round() - // }) - // } - // } - - // pub fn format_kbytes_range(&self, a: usize, b: usize) -> String { - // let min_bytes = a.min(b); - // let max_bytes = a.max(b); - - // // if the range is small, just display the upper number - // if min_bytes * 4 > max_bytes * 3 || max_bytes < 250_000 { - // return self.format_kbytes(max_bytes); - // } - - // let (denom, unit) = match max_bytes { - // 0..=800_000 => (1000., "KB"), - // _ => (1_000_000., "MB"), - // }; - // let mut low_val = min_bytes as f64 / denom; - // let high_val = max_bytes as f64 / denom; - // if low_val > 1. && high_val > 10. { - // low_val = low_val.round(); // spread is so high that precision of low end isn't relevant - // } - // format!("{}–{}{}", Self::format_number_frac(low_val), Self::format_number_frac(high_val), unit) - // } - - // /// Display number 0..1 as percent - // pub fn format_fraction(&self, num: f64) -> String { - // if num < 1.9 { - // format!("{:0.1}%", num) - // } else { - // format!("{}%", Numeric::english().format_int(num.round() as usize)) - // } - // } - - // pub fn format(date: &DateTime<FixedOffset>) -> String { - // date.format("%b %e, %Y").to_string() - // } - - // pub fn format_month(date: &DateTime<FixedOffset>) -> String { - // date.format("%b %Y").to_string() - // } } |