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

Team page #790

Merged
merged 16 commits into from
Jun 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions app/controllers/team.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.crates.meta.total'),

currentSortBy: computed('sort', function() {
return (this.get('sort') === 'downloads') ? 'Downloads' : 'Alphabetical';
}),
});
2 changes: 2 additions & 0 deletions app/models/crate.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export default DS.Model.extend({
badge_sort: ['badge_type'],
annotated_badges: Ember.computed.sort('enhanced_badges', 'badge_sort'),
owners: DS.hasMany('users', { async: true }),
owner_team: DS.hasMany('teams', { async: true }),
owner_user: 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 }),
Expand Down
17 changes: 17 additions & 0 deletions app/models/team.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import DS from 'ember-data';
import Ember from 'ember';

export default DS.Model.extend({
email: DS.attr('string'),
name: DS.attr('string'),
login: DS.attr('string'),
api_token: DS.attr('string'),
avatar: DS.attr('string'),
url: DS.attr('string'),
kind: DS.attr('string'),
org_name: Ember.computed('login', function() {
let login = this.get('login');
let login_split = login.split(':');
return login_split[1];
})
});
1 change: 1 addition & 0 deletions app/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export default DS.Model.extend({
api_token: DS.attr('string'),
avatar: DS.attr('string'),
url: DS.attr('string'),
kind: DS.attr('string'),
});
1 change: 1 addition & 0 deletions app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Router.map(function() {
});
this.route('category_slugs');
this.route('catchAll', { path: '*path' });
this.route('team', { path: '/teams/:team_id' });
});

export default Router;
37 changes: 37 additions & 0 deletions app/routes/team.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Ember from 'ember';

export default Ember.Route.extend({
queryParams: {
page: { refreshedModel: true },
sort: { refreshedModel: true },
},
data: {},

setupController(controller, model) {
this._super(controller, model);

controller.set('fetchingFeed', true);
controller.set('crates', this.get('data.crates'));
},

model(params) {
const { team_id } = params;
return this.store.find('team', team_id).then(
(team) => {
params.team_id = team.get('id');
return Ember.RSVP.hash({
crates: this.store.query('crate', params),
team
});
},
(e) => {
if (e.errors.any(e => e.detail === 'Not Found')) {
this
.controllerFor('application')
.set('nextFlashError', `User '${params.team_id}' does not exist`);
return this.replaceWith('index');
}
}
);
},
});
3 changes: 3 additions & 0 deletions app/styles/crate.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
img {
@include align-self(center);
}
.team-info {
@include flex-direction(column);
}
}
h1 {
padding-left: 10px;
Expand Down
16 changes: 12 additions & 4 deletions app/templates/crate/version.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,19 @@
<div>
<h3>Owners</h3>
<ul class='owners'>
{{#each crate.owners as |owner|}}
{{#each crate.owner_team as |team|}}
<li>
{{#link-to 'user' owner.login}}
{{user-avatar user=owner size='medium-small'}}
{{/link-to}}
{{#link-to team.kind team.login}}
{{user-avatar user=team size='medium-small'}}
{{/link-to}}
</li>
{{/each}}

{{#each crate.owner_user as |user|}}
<li>
{{#link-to user.kind user.login}}
{{user-avatar user=user size='medium-small'}}
{{/link-to}}
</li>
{{/each}}
</ul>
Expand Down
76 changes: 76 additions & 0 deletions app/templates/team.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<div id='crates-heading'>
<div class="wide">
<div class='info'>
{{user-avatar user=model.team size='medium'}}
<div class='team-info'>
<h1>
{{ model.team.org_name }}
</h1>
<h2>
{{ model.team.name }}
</h2>
</div>
{{#user-link user=model.team}}
<img alt="GitHub profile" title="GitHub profile" src="/assets/GitHub-Mark-32px.png"/>
{{/user-link}}
</div>
</div>
</div>

<div id='user-profile'>
<div class='info'>
{{! TODO: reduce duplication with templates/crates.hbs }}

<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.crates 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>
</div>
</div>
22 changes: 21 additions & 1 deletion mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import crateFixture from '../mirage/fixtures/crate';
import crateVersionsFixture from '../mirage/fixtures/crate_versions';
import crateAuthorsFixture from '../mirage/fixtures/crate_authors';
import crateOwnersFixture from '../mirage/fixtures/crate_owners';
import crateTeamsFixture from '../mirage/fixtures/crate_teams';
import crateReverseDependenciesFixture from '../mirage/fixtures/crate_reverse_dependencies';
import crateDependenciesFixture from '../mirage/fixtures/crate_dependencies';
import crateDownloadsFixture from '../mirage/fixtures/crate_downloads';
import keywordFixture from '../mirage/fixtures/keyword';
import teamFixture from '../mirage/fixtures/team';
import userFixture from '../mirage/fixtures/user';

export default function() {
this.get('/summary', () => summaryFixture);
Expand All @@ -20,6 +23,20 @@ export default function() {
crates: searchFixture.crates.slice(start, end),
meta: searchFixture.meta,
};
} else if (request.queryParams.team_id) {
const { start, end } = pageParams(request);
return {
team: teamFixture.team,
crates: searchFixture.crates.slice(start, end),
meta: searchFixture.meta,
};
} else if (request.queryParams.user_id) {
const { start, end } = pageParams(request);
return {
user: userFixture.user,
crates: searchFixture.crates.slice(start, end),
meta: searchFixture.meta,
};
}
});

Expand All @@ -28,11 +45,14 @@ export default function() {
this.get('/api/v1/crates/nanomsg', () => crateFixture);
this.get('/api/v1/crates/nanomsg/versions', () => crateVersionsFixture);
this.get('/api/v1/crates/nanomsg/:version_num/authors', () => crateAuthorsFixture);
this.get('/api/v1/crates/nanomsg/owners', () => crateOwnersFixture);
this.get('/api/v1/crates/nanomsg/owner_user', () => crateOwnersFixture);
this.get('/api/v1/crates/nanomsg/owner_team', () => crateTeamsFixture);
this.get('/api/v1/crates/nanomsg/reverse_dependencies', () => crateReverseDependenciesFixture);
this.get('/api/v1/crates/nanomsg/:version_num/dependencies', () => crateDependenciesFixture);
this.get('/api/v1/crates/nanomsg/downloads', () => crateDownloadsFixture);
this.get('/api/v1/keywords/network', () => keywordFixture);
this.get('/api/v1/teams/:team_id', () => teamFixture);
this.get('/api/v1/users/:user_id', () => userFixture);
}

function pageParams(request) {
Expand Down
3 changes: 2 additions & 1 deletion mirage/fixtures/crate.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export default {
],
"license": "MIT",
"links": {
"owners": "/api/v1/crates/nanomsg/owners",
"owner_user": "/api/v1/crates/nanomsg/owner_user",
"owner_team": "/api/v1/crates/nanomsg/owner_team",
"reverse_dependencies": "/api/v1/crates/nanomsg/reverse_dependencies",
"version_downloads": "/api/v1/crates/nanomsg/downloads",
"versions": null
Expand Down
23 changes: 23 additions & 0 deletions mirage/fixtures/crate_teams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// jscs:disable validateQuoteMarks
export default {
"teams": [
{
"avatar": "https://avatars.githubusercontent.com/u/565790?v=3",
"email": "someone@example.com",
"id": 2,
"kind": "team",
"login": "github:org:thehydroimpulse",
"name": "Team Daniel Fagnan",
"url": "https://github.com/thehydroimpulse"
},
{
"avatar": "https://avatars.githubusercontent.com/u/9447137?v=3",
"email": null,
"id": 303,
"kind": "team",
"login": "github:org:blabaere",
"name": "Team Benoît Labaere",
"url": "https://github.com/blabaere"
}
]
};
10 changes: 10 additions & 0 deletions mirage/fixtures/team.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// jscs:disable validateQuoteMarks
export default {
"team": {
"avatar": "https://avatars.githubusercontent.com/u/565790?v=3",
"id": 1,
"login": "github:org_test:thehydroimpulseteam",
"name": "thehydroimpulseteam",
"url": "https://github.com/thehydroimpulse",
}
};
11 changes: 11 additions & 0 deletions mirage/fixtures/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// jscs:disable validateQuoteMarks
export default {
"user": {
"avatar": "https://avatars.githubusercontent.com/u/565790?v=3",
"email": "someone@example.com",
"id": 1,
"login": "thehydroimpulse",
"name": "Daniel Fagnan",
"url": "https://github.com/thehydroimpulse"
}
};
47 changes: 47 additions & 0 deletions src/krate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ pub struct CrateLinks {
pub version_downloads: String,
pub versions: Option<String>,
pub owners: Option<String>,
pub owner_team: Option<String>,
pub owner_user: Option<String>,
pub reverse_dependencies: String,
}

Expand Down Expand Up @@ -535,6 +537,8 @@ impl Crate {
version_downloads: format!("/api/v1/crates/{}/downloads", name),
versions: versions_link,
owners: Some(format!("/api/v1/crates/{}/owners", name)),
owner_team: Some(format!("/api/v1/crates/{}/owner_team", name)),
owner_user: Some(format!("/api/v1/crates/{}/owner_user", name)),
reverse_dependencies: format!("/api/v1/crates/{}/reverse_dependencies", name),
},
}
Expand Down Expand Up @@ -864,6 +868,15 @@ pub fn index(req: &mut Request) -> CargoResult<Response> {
.filter(crate_owners::owner_kind.eq(OwnerKind::User as i32)),
),
);
} else if let Some(team_id) = params.get("team_id").and_then(|s| s.parse::<i32>().ok()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to put this on index instead of adding a new route?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with how getting a user's crates works, mostly. If we're going to pull this out into its own route, then I think users should be pulled out too, which imo should be a separate PR. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that users should be pulled out as well, but I don't think that should block this PR being a separate route, if it's not significantly more work for this PR.

query = query.filter(
crates::id.eq_any(
crate_owners::table
.select(crate_owners::crate_id)
.filter(crate_owners::owner_id.eq(team_id))
.filter(crate_owners::owner_kind.eq(OwnerKind::Team as i32)),
),
);
} else if params.get("following").is_some() {
query = query.filter(crates::id.eq_any(
follows::table.select(follows::crate_id).filter(
Expand Down Expand Up @@ -1423,6 +1436,40 @@ pub fn owners(req: &mut Request) -> CargoResult<Response> {
Ok(req.json(&R { users: owners }))
}

/// Handles the `GET /crates/:crate_id/owner_team` route.
pub fn owner_team(req: &mut Request) -> CargoResult<Response> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about making this a single route and adding a ?kind parameter if needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We tried, but the structure of the response has user: vs team: change that ember cares about, and I suck at ember and I'm not sure why it cares about that. If we duplicate the response structure too, then it's most of the function that's still duplicated :-/

I wouldn't be opposed to refactoring this in the future, it would just take me more time in ember than we'd like to spend right now :P

let crate_name = &req.params()["crate_id"];
let conn = req.db_conn()?;
let krate = Crate::by_name(crate_name).first::<Crate>(&*conn)?;
let owners = Team::owning(&krate, &conn)?
.into_iter()
.map(Owner::encodable)
.collect();

#[derive(RustcEncodable)]
struct R {
teams: Vec<EncodableOwner>,
}
Ok(req.json(&R { teams: owners }))
}

/// Handles the `GET /crates/:crate_id/owner_user` route.
pub fn owner_user(req: &mut Request) -> CargoResult<Response> {
let crate_name = &req.params()["crate_id"];
let conn = req.db_conn()?;
let krate = Crate::by_name(crate_name).first::<Crate>(&*conn)?;
let owners = User::owning(&krate, &conn)?
.into_iter()
.map(Owner::encodable)
.collect();

#[derive(RustcEncodable)]
struct R {
users: Vec<EncodableOwner>,
}
Ok(req.json(&R { users: owners }))
}

/// Handles the `PUT /crates/:crate_id/owners` route.
pub fn add_owners(req: &mut Request) -> CargoResult<Response> {
modify_owners(req, true)
Expand Down
Loading