diff --git a/app/models/crate.js b/app/models/crate.js index 59cbf21bf24..f7401510a73 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -8,6 +8,7 @@ export default Model.extend({ created_at: attr('date'), updated_at: attr('date'), max_version: attr('string'), + newest_version: attr('string'), description: attr('string'), homepage: attr('string'), diff --git a/app/templates/components/crate-list.hbs b/app/templates/components/crate-list-name-only.hbs similarity index 81% rename from app/templates/components/crate-list.hbs rename to app/templates/components/crate-list-name-only.hbs index f4c69d02739..63be00ae384 100644 --- a/app/templates/components/crate-list.hbs +++ b/app/templates/components/crate-list-name-only.hbs @@ -2,7 +2,7 @@ {{#each this.crates as |crate index|}}
  • - {{ crate.name }} ({{ crate.max_version }}) + {{ crate.name }}
    {{svg-jar "right-arrow"}}
    diff --git a/app/templates/components/crate-list-newest.hbs b/app/templates/components/crate-list-newest.hbs new file mode 100644 index 00000000000..39a7659743e --- /dev/null +++ b/app/templates/components/crate-list-newest.hbs @@ -0,0 +1,12 @@ +
      + {{#each this.crates as |crate index|}} +
    1. + + {{ crate.name }} ({{ crate.newest_version }}) +
      + {{svg-jar "right-arrow"}} +
      +
      +
    2. + {{/each}} +
    diff --git a/app/templates/index.hbs b/app/templates/index.hbs index 9a32236f93b..4b6e4d401a3 100644 --- a/app/templates/index.hbs +++ b/app/templates/index.hbs @@ -38,19 +38,19 @@

    New Crates

    - +

    Most Downloaded

    - +

    Just Updated

    - +

    Most Recent Downloads

    - +

    Popular Keywords (see all)

    diff --git a/mirage/factories/crate.js b/mirage/factories/crate.js index f354a1d5aa5..899f2a0dd02 100644 --- a/mirage/factories/crate.js +++ b/mirage/factories/crate.js @@ -16,6 +16,7 @@ export default Factory.extend({ homepage: () => faker.internet.url(), repository: () => faker.internet.url(), max_version: () => faker.system.semver(), + newest_version: () => faker.system.semver(), created_at: () => faker.date.past(), updated_at() { diff --git a/mirage/fixtures/crates.js b/mirage/fixtures/crates.js index be295dacba3..8ec46073fa6 100644 --- a/mirage/fixtures/crates.js +++ b/mirage/fixtures/crates.js @@ -17,6 +17,7 @@ export default [ id: 'kinetic-rust', keywords: ['Protocol', 'Kinetic', 'Storage'], max_version: '0.0.16', + newest_version: '0.0.16', name: 'kinetic-rust', repository: 'https://github.com/icorderi/kinetic-rust/', updated_at: '2015-04-21T00:15:49Z', @@ -41,6 +42,7 @@ export default [ id: 'nanomsg', keywords: ['network'], max_version: '0.7.0-alpha', + newest_version: '0.6.1', name: 'nanomsg', repository: 'https://github.com/thehydroimpulse/nanomsg.rs', updated_at: '2016-12-28T08:40:00Z', @@ -70,6 +72,7 @@ export default [ id: 'rust_mixin', keywords: ['rust', 'plugin', 'code-generation'], max_version: '0.0.1', + newest_version: '0.0.1', name: 'rust_mixin', repository: 'https://github.com/huonw/external_mixin', updated_at: '2015-02-27T11:52:13Z', @@ -142,6 +145,7 @@ export default [ id: 'external_mixin', keywords: ['python', 'ruby', 'shell', 'plugin', 'code-generation'], max_version: '0.0.1', + newest_version: '0.0.1', name: 'external_mixin', repository: 'https://github.com/huonw/external_mixin', updated_at: '2015-02-27T11:51:58Z', @@ -157,6 +161,7 @@ export default [ id: 'external_mixin_umbrella', keywords: ['plugin', 'code-generation'], max_version: '0.0.2', + newest_version: '0.0.2', name: 'external_mixin_umbrella', repository: 'https://github.com/huonw/external_mixin', updated_at: '2015-02-27T11:52:30Z', @@ -173,6 +178,7 @@ export default [ id: 'Inflector', keywords: ['string', 'case', 'camel', 'snake', 'inflection'], max_version: '0.1.6', + newest_version: '0.1.6', name: 'Inflector', repository: 'https://github.com/whatisinternet/inflector', updated_at: '2015-10-27T01:51:42Z', @@ -188,6 +194,7 @@ export default [ id: 'rs-es', keywords: ['elasticsearch', 'elastic'], max_version: '0.1.17', + newest_version: '0.1.17', name: 'rs-es', repository: 'https://github.com/benashford/rs-es', updated_at: '2015-09-09T15:34:50Z', @@ -203,6 +210,7 @@ export default [ id: 'rust-crypto', keywords: ['Crypto', 'MD5', 'Sha1', 'Sha2', 'AES'], max_version: '0.2.34', + newest_version: '0.2.34', name: 'rust-crypto', repository: 'https://github.com/DaGenix/rust-crypto/', updated_at: '2015-10-29T01:16:17Z', @@ -218,6 +226,7 @@ export default [ id: 'rust-htslib', keywords: ['htslib', 'bam', 'bioinformatics', 'pileup', 'sequencing'], max_version: '0.5.2', + newest_version: '0.5.2', name: 'rust-htslib', repository: 'https://github.com/rust-bio/rust-htslib.git', updated_at: '2015-11-11T00:10:43Z', @@ -233,6 +242,7 @@ export default [ id: 'rustless', keywords: ['api', 'web', 'hyper', 'iron', 'rest'], max_version: '0.8.0', + newest_version: '0.8.0', name: 'rustless', repository: 'https://crates.io/crates/rustless', updated_at: '2015-10-31T11:49:29Z', @@ -248,6 +258,7 @@ export default [ id: 'serde', keywords: ['serde', 'serialization'], max_version: '0.6.1', + newest_version: '0.6.1', name: 'serde', repository: 'https://github.com/serde-rs/serde', updated_at: '2015-10-18T03:10:21Z', @@ -263,6 +274,7 @@ export default [ id: 'rusted_cypher', keywords: ['neo4j', 'database', 'query', 'cypher', 'graph'], max_version: '0.7.1', + newest_version: '0.7.1', name: 'rusted_cypher', repository: 'https://github.com/livioribeiro/rusted-cypher', updated_at: '2015-11-07T17:26:55Z', @@ -279,6 +291,7 @@ export default [ id: 'zlib', keywords: [], max_version: '0.0.1', + newest_version: '0.0.1', name: 'zlib', repository: null, updated_at: '2015-01-02T20:54:04Z', @@ -295,6 +308,7 @@ export default [ id: 'rustful', keywords: ['web', 'rest', 'framework', 'http', 'routing'], max_version: '0.5.0', + newest_version: '0.5.0', name: 'rustful', repository: 'https://github.com/Ogeon/rustful', updated_at: '2015-09-19T21:10:27Z', @@ -310,6 +324,7 @@ export default [ id: 'postgres', keywords: ['database', 'sql'], max_version: '0.10.1', + newest_version: '0.10.1', name: 'postgres', repository: 'https://github.com/sfackler/rust-postgres', updated_at: '2015-11-08T00:48:59Z', @@ -325,6 +340,7 @@ export default [ id: 'quickcheck', keywords: ['testing', 'quickcheck', 'property', 'shrinking', 'fuzz'], max_version: '0.2.24', + newest_version: '0.2.24', name: 'quickcheck', repository: 'https://github.com/BurntSushi/quickcheck', updated_at: '2015-09-20T21:53:38Z', @@ -340,6 +356,7 @@ export default [ id: 'quickcheck_macros', keywords: ['testing', 'quickcheck', 'property', 'shrinking', 'fuzz'], max_version: '0.2.24', + newest_version: '0.2.24', name: 'quickcheck_macros', repository: 'https://github.com/BurntSushi/quickcheck', updated_at: '2015-09-20T21:53:57Z', @@ -356,6 +373,7 @@ export default [ id: 'nc_rustlex', keywords: ['lexer', 'lexical', 'analyser', 'generator'], max_version: '0.3.1', + newest_version: '0.3.1', name: 'nc_rustlex', repository: 'https://github.com/nicolas-cherel/rustlex', updated_at: '2015-08-25T19:15:35Z', @@ -371,6 +389,7 @@ export default [ id: 'nom', keywords: ['parser', 'parser-combinators', 'parsing', 'streaming', 'bit'], max_version: '1.0.1', + newest_version: '1.0.1', name: 'nom', repository: 'https://github.com/Geal/nom', updated_at: '2015-11-22T22:00:41Z', diff --git a/mirage/fixtures/versions.js b/mirage/fixtures/versions.js index 5a6a7347ed1..e7aaa14bd6f 100644 --- a/mirage/fixtures/versions.js +++ b/mirage/fixtures/versions.js @@ -1,7 +1,7 @@ export default [ { crate: 'nanomsg', - created_at: '2016-12-27T08:40:00Z', + created_at: '2016-12-20T07:30:00Z', dl_path: '/api/v1/crates/nanomsg/0.7.0-alpha.1/download', downloads: 260, features: { @@ -9,7 +9,7 @@ export default [ }, id: 40906, num: '0.7.0-alpha.1', - updated_at: '2016-12-27T08:40:00Z', + updated_at: '2016-12-20T07:30:00Z', yanked: false, license: 'MIT', crate_size: 912355, diff --git a/mirage/serializers/crate.js b/mirage/serializers/crate.js index 940a8f3d27e..ef9ad34957c 100644 --- a/mirage/serializers/crate.js +++ b/mirage/serializers/crate.js @@ -14,6 +14,7 @@ export default BaseSerializer.extend({ 'keywords', 'links', 'max_version', + 'newest_version', 'name', 'repository', 'updated_at', diff --git a/src/controllers/krate/metadata.rs b/src/controllers/krate/metadata.rs index 14e42c2c533..2817ebf9c54 100644 --- a/src/controllers/krate/metadata.rs +++ b/src/controllers/krate/metadata.rs @@ -1,10 +1,11 @@ //! Endpoints that expose metadata about a crate //! -//! These endpoints provide data that could be obtained direclty from the +//! These endpoints provide data that could be obtained directly from the //! index or cached metadata which was extracted (client side) from the //! `Cargo.toml` file. use crate::controllers::frontend_prelude::*; + use crate::models::{ Category, Crate, CrateCategory, CrateKeyword, CrateVersions, Keyword, RecentCrateDownloads, User, Version, VersionOwnerAction, @@ -31,10 +32,10 @@ pub fn summary(req: &mut dyn Request) -> AppResult { versions .grouped_by(&krates) .into_iter() - .map(|versions| Version::max(versions.into_iter().map(|v| v.num))) + .map(|versions| Version::top(versions.into_iter().map(|v| (v.created_at, v.num)))) .zip(krates) - .map(|(max_version, krate)| { - Ok(krate.minimal_encodable(&max_version, None, false, None)) + .map(|(top_versions, krate)| { + Ok(krate.minimal_encodable(&top_versions, None, false, None)) }) .collect() }; @@ -142,7 +143,7 @@ pub fn show(req: &mut dyn Request) -> AppResult { let badges = badges::table .filter(badges::crate_id.eq(krate.id)) .load(&*conn)?; - let max_version = krate.max_version(&conn)?; + let top_versions = krate.top_versions(&conn)?; #[derive(Serialize)] struct R { @@ -154,7 +155,7 @@ pub fn show(req: &mut dyn Request) -> AppResult { } Ok(req.json(&R { krate: krate.clone().encodable( - &max_version, + &top_versions, Some(ids), Some(&kws), Some(&cats), diff --git a/src/controllers/krate/publish.rs b/src/controllers/krate/publish.rs index 09dce9934aa..b3d93a9f672 100644 --- a/src/controllers/krate/publish.rs +++ b/src/controllers/krate/publish.rs @@ -168,7 +168,7 @@ pub fn publish(req: &mut dyn Request) -> AppResult { // Update all badges for this crate, collecting any invalid badges in // order to be able to warn about them let ignored_invalid_badges = Badge::update_crate(&conn, &krate, new_crate.badges.as_ref())?; - let max_version = krate.max_version(&conn)?; + let top_versions = krate.top_versions(&conn)?; if let Some(readme) = new_crate.readme { render::render_and_upload_readme( @@ -215,7 +215,7 @@ pub fn publish(req: &mut dyn Request) -> AppResult { }; Ok(req.json(&GoodCrate { - krate: krate.minimal_encodable(&max_version, None, false, None), + krate: krate.minimal_encodable(&top_versions, None, false, None), warnings, })) }) diff --git a/src/controllers/krate/search.rs b/src/controllers/krate/search.rs index 2ad799bd4ce..05aaa6c9bc1 100644 --- a/src/controllers/krate/search.rs +++ b/src/controllers/krate/search.rs @@ -179,7 +179,7 @@ pub fn search(req: &mut dyn Request) -> AppResult { .load::(&*conn)? .grouped_by(&crates) .into_iter() - .map(|versions| Version::max(versions.into_iter().map(|v| v.num))); + .map(|versions| Version::top(versions.into_iter().map(|v| (v.created_at, v.num)))); let badges = CrateBadge::belonging_to(&crates) .select((badges::crate_id, badges::all_columns)) diff --git a/src/models/krate.rs b/src/models/krate.rs index a6839b19e92..46d035a4dae 100644 --- a/src/models/krate.rs +++ b/src/models/krate.rs @@ -8,12 +8,12 @@ use url::Url; use crate::app::App; use crate::email; -use crate::util::{cargo_err, AppResult}; - +use crate::models::version::TopVersions; use crate::models::{ Badge, Category, CrateOwner, CrateOwnerInvitation, Keyword, NewCrateOwnerInvitation, Owner, OwnerKind, ReverseDependency, User, Version, }; +use crate::util::{cargo_err, AppResult}; use crate::views::{EncodableCrate, EncodableCrateLinks}; use crate::models::helpers::with_count::*; @@ -204,7 +204,7 @@ impl<'a> NewCrate<'a> { } impl Crate { - /// SQL filter based on whether the crate's name loosly matches the given + /// SQL filter based on whether the crate's name loosely matches the given /// string. /// /// The operator used varies based on the input. @@ -281,13 +281,13 @@ impl Crate { pub fn minimal_encodable( self, - max_version: &semver::Version, + top_versions: &TopVersions, badges: Option>, exact_match: bool, recent_downloads: Option, ) -> EncodableCrate { self.encodable( - max_version, + top_versions, None, None, None, @@ -300,7 +300,7 @@ impl Crate { #[allow(clippy::too_many_arguments)] pub fn encodable( self, - max_version: &semver::Version, + top_versions: &TopVersions, versions: Option>, keywords: Option<&[Keyword]>, categories: Option<&[Category]>, @@ -339,7 +339,8 @@ impl Crate { keywords: keyword_ids, categories: category_ids, badges, - max_version: max_version.to_string(), + max_version: top_versions.highest.to_string(), + newest_version: top_versions.newest.to_string(), documentation, homepage, exact_match, @@ -384,16 +385,16 @@ impl Crate { } } - pub fn max_version(&self, conn: &PgConnection) -> AppResult { + /// Return both the newest (most recently updated) and + /// highest version (in semver order) for the current crate. + pub fn top_versions(&self, conn: &PgConnection) -> AppResult { use crate::schema::versions::dsl::*; - let vs = self - .versions() - .select(num) - .load::(conn)? - .into_iter() - .map(|s| semver::Version::parse(&s).unwrap()); - Ok(Version::max(vs)) + Ok(Version::top( + self.versions() + .select((updated_at, num)) + .load::<(NaiveDateTime, semver::Version)>(conn)?, + )) } pub fn owners(&self, conn: &PgConnection) -> AppResult> { diff --git a/src/models/version.rs b/src/models/version.rs index 0ee71275d64..54345f746ff 100644 --- a/src/models/version.rs +++ b/src/models/version.rs @@ -37,6 +37,25 @@ pub struct NewVersion { published_by: i32, } +/// The highest version (semver order) and the most recently updated version. +/// Typically used for a single crate. +#[derive(Debug, Clone)] +pub struct TopVersions { + pub highest: semver::Version, + pub newest: semver::Version, +} + +/// A default semver value, "0.0.0", for use in TopVersions +fn default_semver_version() -> semver::Version { + semver::Version { + major: 0, + minor: 0, + patch: 0, + pre: vec![], + build: vec![], + } +} + impl Version { pub fn encodable( self, @@ -96,20 +115,28 @@ impl Version { .load(conn) } - pub fn max(versions: T) -> semver::Version + /// Return both the newest (most recently updated) and the + /// highest version (in semver order) for a collection of date/version pairs. + pub fn top(pairs: T) -> TopVersions where - T: IntoIterator, + T: Clone + IntoIterator, { - versions - .into_iter() - .max() - .unwrap_or_else(|| semver::Version { - major: 0, - minor: 0, - patch: 0, - pre: vec![], - build: vec![], - }) + TopVersions { + newest: pairs + .clone() + .into_iter() + .max() + .unwrap_or(( + NaiveDateTime::from_timestamp(0, 0), + default_semver_version(), + )) + .1, + highest: pairs + .into_iter() + .map(|(_, v)| v) + .max() + .unwrap_or_else(default_semver_version), + } } pub fn record_readme_rendering(version_id_: i32, conn: &PgConnection) -> QueryResult { diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 82815951f77..f85912dc487 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -1243,6 +1243,12 @@ fn summary_new_crates() { let krate3 = CrateBuilder::new("just_updated", user.id) .version(VersionBuilder::new("0.1.0")) + .version(VersionBuilder::new("0.1.2")) + .expect_build(conn); + + let krate4 = CrateBuilder::new("just_updated_patch", user.id) + .version(VersionBuilder::new("0.1.0")) + .version(VersionBuilder::new("0.2.0")) .expect_build(conn); CrateBuilder::new("with_downloads", user.id) @@ -1269,11 +1275,25 @@ fn summary_new_crates() { .set(crates::updated_at.eq(updated)) .execute(&*conn) .unwrap(); + + let plus_two = Utc::now().naive_utc() + chrono::Duration::seconds(2); + let newer = VersionBuilder::new("0.1.1").expect_build(krate4.id, user.id, conn); + + // Update the patch version to be newer than the other versions, including the higher one. + update(&newer) + .set(versions::created_at.eq(plus_two)) + .execute(&*conn) + .unwrap(); + + update(&krate4) + .set(crates::updated_at.eq(plus_two)) + .execute(&*conn) + .unwrap(); }); let json: SummaryResponse = anon.get("/api/v1/summary").good(); - assert_eq!(json.num_crates, 4); + assert_eq!(json.num_crates, 5); assert_eq!(json.num_downloads, 6000); assert_eq!(json.most_downloaded[0].name, "most_recent_downloads"); assert_eq!( @@ -1282,9 +1302,17 @@ fn summary_new_crates() { ); assert_eq!(json.popular_keywords[0].keyword, "popular"); assert_eq!(json.popular_categories[0].category, "Category 1"); - assert_eq!(json.just_updated.len(), 1); - assert_eq!(json.just_updated[0].name, "just_updated"); - assert_eq!(json.new_crates.len(), 4); + assert_eq!(json.just_updated.len(), 2); + + assert_eq!(json.just_updated[0].name, "just_updated_patch"); + assert_eq!(json.just_updated[0].max_version, "0.2.0"); + assert_eq!(json.just_updated[0].newest_version, "0.1.1"); + + assert_eq!(json.just_updated[1].name, "just_updated"); + assert_eq!(json.just_updated[1].max_version, "0.1.2"); + assert_eq!(json.just_updated[1].newest_version, "0.1.2"); + + assert_eq!(json.new_crates.len(), 5); } #[test] diff --git a/src/tests/util.rs b/src/tests/util.rs index 054a44d92da..76b2e8b180b 100644 --- a/src/tests/util.rs +++ b/src/tests/util.rs @@ -305,7 +305,7 @@ impl TestAppBuilder { } } -/// A colleciton of helper methods for the 3 authentication types +/// A collection of helper methods for the 3 authentication types /// /// Helper methods go through public APIs, and should not modify the database directly pub trait RequestHelper { diff --git a/src/views.rs b/src/views.rs index 981578e8be7..e5b86a8f98b 100644 --- a/src/views.rs +++ b/src/views.rs @@ -97,6 +97,7 @@ pub struct EncodableCrate { pub recent_downloads: Option, // NOTE: Used by shields.io, altering `max_version` requires a PR with shields.io pub max_version: String, + pub newest_version: String, // Most recently updated version, which may not be max pub description: Option, pub homepage: Option, pub documentation: Option, @@ -363,6 +364,7 @@ mod tests { downloads: 0, recent_downloads: None, max_version: "".to_string(), + newest_version: "".to_string(), description: None, homepage: None, documentation: None, diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js index 3bab3e97591..2d4561be7a4 100644 --- a/tests/acceptance/crate-test.js +++ b/tests/acceptance/crate-test.js @@ -61,13 +61,17 @@ module('Acceptance | crate page', function(hooks) { }); test('visiting a crate page from the front page', async function(assert) { - this.server.create('crate', 'withVersion', { id: 'nanomsg' }); + this.server.create('crate', { id: 'nanomsg', newest_version: '0.6.1' }); + this.server.create('version', { crate: 'nanomsg', num: '0.6.1' }); await visit('/'); await click('[data-test-just-updated] [data-test-crate-link="0"]'); - assert.equal(currentURL(), '/crates/nanomsg'); + assert.equal(currentURL(), '/crates/nanomsg/0.6.1'); assert.equal(title(), 'nanomsg - crates.io: Rust Package Registry'); + + assert.dom('[data-test-heading] [data-test-crate-name]').hasText('nanomsg'); + assert.dom('[data-test-heading] [data-test-crate-version]').hasText('0.6.1'); }); test('visiting /crates/nanomsg', async function(assert) { diff --git a/tests/acceptance/front-page-test.js b/tests/acceptance/front-page-test.js index f31eeb9a0b5..8fc023b87b0 100644 --- a/tests/acceptance/front-page-test.js +++ b/tests/acceptance/front-page-test.js @@ -36,13 +36,13 @@ module('Acceptance | front page', function(hooks) { assert.dom('[data-test-total-crates]').hasText('19'); assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasText('Inflector (0.1.6)'); - assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasAttribute('href', '/crates/Inflector'); + assert.dom('[data-test-new-crates] [data-test-crate-link="0"]').hasAttribute('href', '/crates/Inflector/0.1.6'); - assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasText('serde (0.6.1)'); + assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasText('serde'); assert.dom('[data-test-most-downloaded] [data-test-crate-link="0"]').hasAttribute('href', '/crates/serde'); - assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasText('nanomsg (0.7.0-alpha)'); - assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasAttribute('href', '/crates/nanomsg'); + assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasText('nanomsg (0.6.1)'); + assert.dom('[data-test-just-updated] [data-test-crate-link="0"]').hasAttribute('href', '/crates/nanomsg/0.6.1'); percySnapshot(assert); });