summaryrefslogtreecommitdiffstats
path: root/front_end/src/author_page.rs
diff options
context:
space:
mode:
Diffstat (limited to 'front_end/src/author_page.rs')
-rw-r--r--front_end/src/author_page.rs259
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()
- // }
}