Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Categorization! #473

Merged
merged 34 commits into from
Dec 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e18b2f2
Add migrations for storing categories
carols10cents Nov 15, 2016
7893307
Create a binary that syncs categories from a text file
carols10cents Nov 15, 2016
3854dfc
Add a page that lists all categories
carols10cents Nov 16, 2016
55f201f
Add a page that lists the crates in a category
carols10cents Nov 16, 2016
e46971d
Alphabetize mods
carols10cents Nov 16, 2016
9c8ff2d
Add the ability to add a category to a crate while publishing
carols10cents Nov 16, 2016
5e8d7be
Extract a method for encoding a crate without all its metadata
carols10cents Nov 16, 2016
7cb99cf
Extract a test helper method for uploading foo 2.0.0
carols10cents Nov 17, 2016
c8442ed
Show categories on a crate's page
carols10cents Nov 17, 2016
54c374b
Prefer as_str over &[..]
carols10cents Nov 17, 2016
fcd7b28
Prefer type annotations over turbofish
carols10cents Nov 17, 2016
5a8ad82
Prefer not is empty over len greater than zero
carols10cents Nov 17, 2016
6364c73
Move chain_error NotFound into find_by_category
carols10cents Nov 21, 2016
29f8e2e
Use a pretty slug for categories in URLs
carols10cents Nov 21, 2016
d20cfc3
Change the header text on a category page
carols10cents Nov 21, 2016
9c36d77
Return warnings about invalid categories
carols10cents Nov 21, 2016
27b8f95
Add some categories with subcategories
carols10cents Nov 18, 2016
d421dbd
Only show top level categories on the categories index page
carols10cents Nov 19, 2016
82f810c
Show subcategories on a category's page
carols10cents Nov 21, 2016
a3655bd
On a category page, show all crates in all subcategories too
carols10cents Nov 22, 2016
cb7c262
Sync categories on server startup
carols10cents Nov 22, 2016
2dcec87
Use splitn on the category slug/display name pairs
carols10cents Nov 29, 2016
e3e10fd
Add a page listing all valid category slugs
carols10cents Nov 28, 2016
1cee6d8
Make warnings about invalid crate names be JSON instead of text
carols10cents Nov 29, 2016
f7d2780
Switch to using toml for categories instead of plain text
carols10cents Dec 14, 2016
9318605
Add descriptions to categories
carols10cents Dec 14, 2016
f0733d5
Rewrite the categories TOML format and parser
shepmaster Dec 15, 2016
ef33d9c
Fix the query for subcategories to only include the next level
carols10cents Dec 15, 2016
a2531d4
Add a heading for Crates on a category page
carols10cents Dec 15, 2016
c75bb56
Sort crates within a category by downloads by default
carols10cents Dec 15, 2016
c81c950
Sum crate count in all subcategories in a better way
carols10cents Dec 15, 2016
c2d5cf8
Document how to change the categories in the README & toml
carols10cents Dec 15, 2016
c6de914
Use a different crate name in a test to prevent deadlocks
carols10cents Dec 28, 2016
bffd16a
Make all test crates have unique names to prevent test deadlocks
carols10cents Dec 29, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions app/adapters/category-slug.js
Original file line number Diff line number Diff line change
@@ -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);
}
});
17 changes: 17 additions & 0 deletions app/controllers/categories.js
Original file line number Diff line number Diff line change
@@ -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';
}),
});
17 changes: 17 additions & 0 deletions app/controllers/category/index.js
Original file line number Diff line number Diff line change
@@ -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';
}),
});
2 changes: 2 additions & 0 deletions app/controllers/crate/version.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),

Expand Down Expand Up @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions app/models/category-slug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import DS from 'ember-data';

export default DS.Model.extend({
slug: DS.attr('string'),
});
13 changes: 13 additions & 0 deletions app/models/category.js
Original file line number Diff line number Diff line change
@@ -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 })
});
1 change: 1 addition & 0 deletions app/models/crate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
5 changes: 5 additions & 0 deletions app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
});

Expand Down
12 changes: 12 additions & 0 deletions app/routes/categories.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
12 changes: 12 additions & 0 deletions app/routes/category-slugs.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
11 changes: 11 additions & 0 deletions app/routes/category.js
Original file line number Diff line number Diff line change
@@ -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`);
}
});
}
});
18 changes: 18 additions & 0 deletions app/routes/category/index.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
73 changes: 73 additions & 0 deletions app/templates/categories.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{{ title 'Categories' }}

<div id='crates-heading'>
<img class='logo crate' src="/assets/crate.png"/>
<h1>All Categories</h1>
</div>

<div id='results'>
<div class='nav'>
<span class='amt small'>
Displaying
<span class='cur'>{{ currentPageStart }}-{{ currentPageEnd }}</span>
of <span class='total'>{{ totalItems }}</span> total results
</span>
</div>

<div class='sort'>
<span class='small'>Sort by</span>
{{#rl-dropdown-container class="dropdown-container"}}
{{#rl-dropdown-toggle tagName="a" class="dropdown"}}
<img class="sort" src="/assets/sort.png"/>
{{ currentSortBy }}
<span class='arrow'></span>
{{/rl-dropdown-toggle}}

{{#rl-dropdown tagName="ul" class="dropdown" closeOnChildClick="a:link"}}
<li>
{{#link-to (query-params sort="alpha")}}
Alphabetical
{{/link-to}}
</li>
<li>
{{#link-to (query-params sort="crates")}}
# Crates
{{/link-to}}
</li>
{{/rl-dropdown}}
{{/rl-dropdown-container}}
</div>
</div>

<div class='white-rows'>
{{#each model as |category|}}
<div class='row'>
<div class='desc'>
<div class='info'>
{{link-to category.category "category" category.slug}}
<span class='small'>
{{ format-num category.crates_cnt }}
crates
</span>
</div>
<div class='summary'>
<span class='small'>
{{ truncate-text category.description }}
</span>
</div>
</div>
</div>
{{/each}}
</div>

<div class='pagination'>
{{#link-to (query-params page=prevPage) class="prev" rel="prev" title="previous page"}}
<img class="left-pag" src="/assets/left-pag.png"/>
{{/link-to}}
{{#each pages as |page|}}
{{#link-to (query-params page=page)}}{{ page }}{{/link-to}}
{{/each}}
{{#link-to (query-params page=nextPage) class="next" rel="next" title="next page"}}
<img class="right-pag" src="/assets/right-pag.png"/>
{{/link-to}}
</div>
1 change: 1 addition & 0 deletions app/templates/category/error.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ title 'Category Not Found' }}
85 changes: 85 additions & 0 deletions app/templates/category/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{{ title category.category ' - Categories' }}

<div id='crates-heading'>
<img class='logo crate' src="/assets/crate.png"/>
<h1>{{ category.category }}</h1>
</div>

{{#if category.subcategories }}
<div id='subcategories'>
<h2>Subcategories</h2>
<div class='white-rows'>
{{#each category.subcategories as |subcategory| }}
<div class='row'>
<div class='desc'>
<div class='info'>
{{link-to subcategory.category "category" subcategory.slug}}
<span class='small'>
{{ format-num subcategory.crates_cnt }}
crates
</span>
</div>
<div class='summary'>
<span class='small'>
{{ truncate-text subcategory.description }}
</span>
</div>
</div>
</div>
{{/each}}
</div>
</div>
{{/if}}

<h2>Crates</h2>
<div id='results'>
<div class='nav'>
<span class='amt small'>
Displaying
<span class='cur'>{{ currentPageStart }}-{{ currentPageEnd }}</span>
of <span class='total'>{{ totalItems }}</span> total results
</span>
</div>

<div class='sort'>
<span class='small'>Sort by</span>
{{#rl-dropdown-container class="dropdown-container"}}
{{#rl-dropdown-toggle tagName="a" class="dropdown"}}
<img class="sort" src="/assets/sort.png"/>
{{ currentSortBy }}
<span class='arrow'></span>
{{/rl-dropdown-toggle}}

{{#rl-dropdown tagName="ul" class="dropdown" closeOnChildClick="a:link"}}
<li>
{{#link-to (query-params sort="alpha")}}
Alphabetical
{{/link-to}}
</li>
<li>
{{#link-to (query-params sort="downloads")}}
Downloads
{{/link-to}}
</li>
{{/rl-dropdown}}
{{/rl-dropdown-container}}
</div>
</div>

<div id='crates' class='white-rows'>
{{#each model as |crate|}}
{{crate-row crate=crate}}
{{/each}}
</div>

<div class='pagination'>
{{#link-to (query-params page=prevPage) class="prev" rel="prev" title="previous page"}}
<img class="left-pag" src="/assets/left-pag.png"/>
{{/link-to}}
{{#each pages as |page|}}
{{#link-to (query-params page=page)}}{{ page }}{{/link-to}}
{{/each}}
{{#link-to (query-params page=nextPage) class="next" rel="next" title="next page"}}
<img class="right-pag" src="/assets/right-pag.png"/>
{{/link-to}}
</div>
Loading