diff --git a/Cargo.lock b/Cargo.lock index d1145af66e..c4676689ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,7 @@ dependencies = [ "s3 0.0.1", "semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -609,6 +610,14 @@ dependencies = [ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "toml" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "unicode-bidi" version = "0.2.3" @@ -722,6 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" "checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" "checksum time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "3c7ec6d62a20df54e07ab3b78b9a3932972f4b7981de295563686849eb3989af" +"checksum toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "736b60249cb25337bc196faa43ee12c705e426f3d55c214d73a4e7be06f92cb4" "checksum unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c1f7ceb96afdfeedee42bade65a0d585a6a0106f681b6749c8ff4daa8df30b3f" "checksum unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "26643a2f83bac55f1976fb716c10234485f9202dcd65cfbdf9da49867b271172" "checksum url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f024e241a55f5c88401595adc1d4af0c9649e91da82d0e190fe55950231ae575" diff --git a/Cargo.toml b/Cargo.toml index 5ffbf6da6b..9cc60cc91a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ env_logger = "0.3" rustc-serialize = "0.3" license-exprs = "^1.3" dotenv = "0.8.0" +toml = "0.2" conduit = "0.8" conduit-conditional-get = "0.8" diff --git a/README.md b/README.md index 8745e1a2c6..947cdf5002 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,21 @@ follows: ``` cargo test ``` + +## Categories + +The list of categories available on crates.io is stored in +`src/categories.toml`. To propose adding, removing, or changing a category, +send a pull request making the appropriate change to that file as noted in the +comment at the top of the file. Please add a description that will help others +to know what crates are in that category. + +For new categories, it's helpful to note in your PR description examples of +crates that would fit in that category, and describe what distinguishes the new +category from existing categories. + +After your PR is accepted, the next time that crates.io is deployed the +categories will be synced from this file. ## Deploying & Using a Mirror diff --git a/app/adapters/category-slug.js b/app/adapters/category-slug.js new file mode 100644 index 0000000000..109a644709 --- /dev/null +++ b/app/adapters/category-slug.js @@ -0,0 +1,11 @@ +import ApplicationAdapter from './application'; +import Ember from 'ember'; + +export default ApplicationAdapter.extend({ + pathForType(modelName) { + var decamelized = Ember.String.underscore( + Ember.String.decamelize(modelName) + ); + return Ember.String.pluralize(decamelized); + } +}); diff --git a/app/controllers/categories.js b/app/controllers/categories.js new file mode 100644 index 0000000000..5cfc81b0f8 --- /dev/null +++ b/app/controllers/categories.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; +import PaginationMixin from '../mixins/pagination'; + +const { computed } = Ember; + +export default Ember.Controller.extend(PaginationMixin, { + queryParams: ['page', 'per_page', 'sort'], + page: '1', + per_page: 10, + sort: 'alpha', + + totalItems: computed.readOnly('model.meta.total'), + + currentSortBy: computed('sort', function() { + return (this.get('sort') === 'crates') ? '# Crates' : 'Alphabetical'; + }), +}); diff --git a/app/controllers/category/index.js b/app/controllers/category/index.js new file mode 100644 index 0000000000..bbc85947a7 --- /dev/null +++ b/app/controllers/category/index.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; +import PaginationMixin from '../../mixins/pagination'; + +const { computed } = Ember; + +export default Ember.Controller.extend(PaginationMixin, { + queryParams: ['page', 'per_page', 'sort'], + page: '1', + per_page: 10, + sort: 'downloads', + + totalItems: computed.readOnly('model.meta.total'), + + currentSortBy: computed('sort', function() { + return (this.get('sort') === 'downloads') ? 'Downloads' : 'Alphabetical'; + }), +}); diff --git a/app/controllers/crate/version.js b/app/controllers/crate/version.js index ad0c5836a8..99216589ed 100644 --- a/app/controllers/crate/version.js +++ b/app/controllers/crate/version.js @@ -19,6 +19,7 @@ export default Ember.Controller.extend({ currentVersion: computed.alias('model'), requestedVersion: null, keywords: computed.alias('crate.keywords'), + categories: computed.alias('crate.categories'), sortedVersions: computed.readOnly('crate.versions'), @@ -49,6 +50,7 @@ export default Ember.Controller.extend({ }), anyKeywords: computed.gt('keywords.length', 0), + anyCategories: computed.gt('categories.length', 0), currentDependencies: computed('currentVersion.dependencies', function() { var deps = this.get('currentVersion.dependencies'); diff --git a/app/models/category-slug.js b/app/models/category-slug.js new file mode 100644 index 0000000000..167c436b97 --- /dev/null +++ b/app/models/category-slug.js @@ -0,0 +1,5 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + slug: DS.attr('string'), +}); \ No newline at end of file diff --git a/app/models/category.js b/app/models/category.js new file mode 100644 index 0000000000..3be263e0b1 --- /dev/null +++ b/app/models/category.js @@ -0,0 +1,13 @@ +import DS from 'ember-data'; + +export default DS.Model.extend({ + category: DS.attr('string'), + slug: DS.attr('string'), + description: DS.attr('string'), + created_at: DS.attr('date'), + crates_cnt: DS.attr('number'), + + subcategories: DS.attr(), + + crates: DS.hasMany('crate', { async: true }) +}); diff --git a/app/models/crate.js b/app/models/crate.js index 73061e89a1..9065b323f7 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -20,6 +20,7 @@ export default DS.Model.extend({ owners: DS.hasMany('users', { async: true }), version_downloads: DS.hasMany('version-download', { async: true }), keywords: DS.hasMany('keywords', { async: true }), + categories: DS.hasMany('categories', { async: true }), reverse_dependencies: DS.hasMany('dependency', { async: true }), follow() { diff --git a/app/router.js b/app/router.js index 8e48c6c8ca..ee35611828 100644 --- a/app/router.js +++ b/app/router.js @@ -35,6 +35,11 @@ Router.map(function() { this.route('keyword', { path: '/keywords/:keyword_id' }, function() { this.route('index', { path: '/' }); }); + this.route('categories'); + this.route('category', { path: '/categories/:category_id' }, function() { + this.route('index', { path: '/' }); + }); + this.route('category_slugs'); this.route('catchAll', { path: '*path' }); }); diff --git a/app/routes/categories.js b/app/routes/categories.js new file mode 100644 index 0000000000..a3b07940be --- /dev/null +++ b/app/routes/categories.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + queryParams: { + page: { refreshModel: true }, + sort: { refreshModel: true }, + }, + + model(params) { + return this.store.query('category', params); + }, +}); diff --git a/app/routes/category-slugs.js b/app/routes/category-slugs.js new file mode 100644 index 0000000000..8416e4d23d --- /dev/null +++ b/app/routes/category-slugs.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + queryParams: { + page: { refreshModel: true }, + sort: { refreshModel: true }, + }, + + model(params) { + return this.store.query('category-slug', params); + }, +}); diff --git a/app/routes/category.js b/app/routes/category.js new file mode 100644 index 0000000000..a9ca79343d --- /dev/null +++ b/app/routes/category.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + model(params) { + return this.store.find('category', params.category_id).catch(e => { + if (e.errors.any(e => e.detail === 'Not Found')) { + this.controllerFor('application').set('flashError', `Category '${params.category_id}' does not exist`); + } + }); + } +}); diff --git a/app/routes/category/index.js b/app/routes/category/index.js new file mode 100644 index 0000000000..fd16edd91d --- /dev/null +++ b/app/routes/category/index.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; + +export default Ember.Route.extend({ + queryParams: { + page: { refreshModel: true }, + sort: { refreshModel: true }, + }, + + model(params) { + params.category = this.modelFor('category').id; + return this.store.query('crate', params); + }, + + setupController(controller, model) { + controller.set('category', this.modelFor('category')); + this._super(controller, model); + }, +}); diff --git a/app/templates/categories.hbs b/app/templates/categories.hbs new file mode 100644 index 0000000000..16689077a8 --- /dev/null +++ b/app/templates/categories.hbs @@ -0,0 +1,73 @@ +{{ title 'Categories' }} + +
+ +

All Categories

+
+ +
+ + +
+ Sort by + {{#rl-dropdown-container class="dropdown-container"}} + {{#rl-dropdown-toggle tagName="a" class="dropdown"}} + + {{ currentSortBy }} + + {{/rl-dropdown-toggle}} + + {{#rl-dropdown tagName="ul" class="dropdown" closeOnChildClick="a:link"}} +
  • + {{#link-to (query-params sort="alpha")}} + Alphabetical + {{/link-to}} +
  • +
  • + {{#link-to (query-params sort="crates")}} + # Crates + {{/link-to}} +
  • + {{/rl-dropdown}} + {{/rl-dropdown-container}} +
    +
    + +
    + {{#each model as |category|}} +
    +
    +
    + {{link-to category.category "category" category.slug}} + + {{ format-num category.crates_cnt }} + crates + +
    +
    + + {{ truncate-text category.description }} + +
    +
    +
    + {{/each}} +
    + + diff --git a/app/templates/category/error.hbs b/app/templates/category/error.hbs new file mode 100644 index 0000000000..d3e3b5bbcc --- /dev/null +++ b/app/templates/category/error.hbs @@ -0,0 +1 @@ +{{ title 'Category Not Found' }} \ No newline at end of file diff --git a/app/templates/category/index.hbs b/app/templates/category/index.hbs new file mode 100644 index 0000000000..6bd0f2812d --- /dev/null +++ b/app/templates/category/index.hbs @@ -0,0 +1,85 @@ +{{ title category.category ' - Categories' }} + +
    + +

    {{ category.category }}

    +
    + +{{#if category.subcategories }} +
    +

    Subcategories

    +
    + {{#each category.subcategories as |subcategory| }} +
    +
    +
    + {{link-to subcategory.category "category" subcategory.slug}} + + {{ format-num subcategory.crates_cnt }} + crates + +
    +
    + + {{ truncate-text subcategory.description }} + +
    +
    +
    + {{/each}} +
    +
    +{{/if}} + +

    Crates

    +
    + + +
    + Sort by + {{#rl-dropdown-container class="dropdown-container"}} + {{#rl-dropdown-toggle tagName="a" class="dropdown"}} + + {{ currentSortBy }} + + {{/rl-dropdown-toggle}} + + {{#rl-dropdown tagName="ul" class="dropdown" closeOnChildClick="a:link"}} +
  • + {{#link-to (query-params sort="alpha")}} + Alphabetical + {{/link-to}} +
  • +
  • + {{#link-to (query-params sort="downloads")}} + Downloads + {{/link-to}} +
  • + {{/rl-dropdown}} + {{/rl-dropdown-container}} +
    +
    + +
    + {{#each model as |crate|}} + {{crate-row crate=crate}} + {{/each}} +
    + + diff --git a/app/templates/category_slugs.hbs b/app/templates/category_slugs.hbs new file mode 100644 index 0000000000..6e36210b1c --- /dev/null +++ b/app/templates/category_slugs.hbs @@ -0,0 +1,14 @@ +{{ title 'Category Slugs' }} + +
    + +

    All Valid Category Slugs

    +
    + +
    + +
    \ No newline at end of file diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs index 42356ec38e..419f52d5a8 100644 --- a/app/templates/crate/version.hbs +++ b/app/templates/crate/version.hbs @@ -104,6 +104,19 @@ {{/if}} {{/unless}} + {{#unless crate.categories.isPending}} + {{#if anyCategories}} +
    +

    Categories

    + +
    + {{/if}} + {{/unless}} +

    Owners