diff --git a/app/controllers/team.js b/app/controllers/team.js
new file mode 100644
index 00000000000..06705f6e39e
--- /dev/null
+++ b/app/controllers/team.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.crates.meta.total'),
+
+ currentSortBy: computed('sort', function() {
+ return (this.get('sort') === 'downloads') ? 'Downloads' : 'Alphabetical';
+ }),
+});
diff --git a/app/models/crate.js b/app/models/crate.js
index 82457470419..3c7fda4e962 100644
--- a/app/models/crate.js
+++ b/app/models/crate.js
@@ -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 }),
diff --git a/app/models/team.js b/app/models/team.js
new file mode 100644
index 00000000000..b1aef428566
--- /dev/null
+++ b/app/models/team.js
@@ -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];
+ })
+});
diff --git a/app/models/user.js b/app/models/user.js
index d1a736e1a76..3ea87236bc7 100644
--- a/app/models/user.js
+++ b/app/models/user.js
@@ -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'),
});
diff --git a/app/router.js b/app/router.js
index fc7ce9fa751..189b04cf417 100644
--- a/app/router.js
+++ b/app/router.js
@@ -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;
diff --git a/app/routes/team.js b/app/routes/team.js
new file mode 100644
index 00000000000..631e57751e4
--- /dev/null
+++ b/app/routes/team.js
@@ -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');
+ }
+ }
+ );
+ },
+});
diff --git a/app/styles/crate.scss b/app/styles/crate.scss
index 3645a388037..896329dff8f 100644
--- a/app/styles/crate.scss
+++ b/app/styles/crate.scss
@@ -22,6 +22,9 @@
img {
@include align-self(center);
}
+ .team-info {
+ @include flex-direction(column);
+ }
}
h1 {
padding-left: 10px;
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs
index df660b65f02..f6fd050c8d4 100644
--- a/app/templates/crate/version.hbs
+++ b/app/templates/crate/version.hbs
@@ -148,11 +148,19 @@
Owners
- {{#each crate.owners as |owner|}}
+ {{#each crate.owner_team as |team|}}
-
- {{#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}}
+
+ {{/each}}
+
+ {{#each crate.owner_user as |user|}}
+ -
+ {{#link-to user.kind user.login}}
+ {{user-avatar user=user size='medium-small'}}
+ {{/link-to}}
{{/each}}
diff --git a/app/templates/team.hbs b/app/templates/team.hbs
new file mode 100644
index 00000000000..76aba4c7f7f
--- /dev/null
+++ b/app/templates/team.hbs
@@ -0,0 +1,76 @@
+
+
+
+ {{user-avatar user=model.team size='medium'}}
+
+
+ {{ model.team.org_name }}
+
+
+ {{ model.team.name }}
+
+
+ {{#user-link user=model.team}}
+
+ {{/user-link}}
+
+
+
+
+
+
+ {{! TODO: reduce duplication with templates/crates.hbs }}
+
+
+
+
+ Displaying
+ {{ currentPageStart }}-{{ currentPageEnd }}
+ of {{ totalItems }} total results
+
+
+
+
+
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.crates as |crate|}}
+ {{crate-row crate=crate}}
+ {{/each}}
+
+
+
+
+
diff --git a/mirage/config.js b/mirage/config.js
index befc6e001a3..40e17bf9f60 100644
--- a/mirage/config.js
+++ b/mirage/config.js
@@ -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);
@@ -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,
+ };
}
});
@@ -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) {
diff --git a/mirage/fixtures/crate.js b/mirage/fixtures/crate.js
index 51ea63abf62..330ebd45dbf 100644
--- a/mirage/fixtures/crate.js
+++ b/mirage/fixtures/crate.js
@@ -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
diff --git a/mirage/fixtures/crate_teams.js b/mirage/fixtures/crate_teams.js
new file mode 100644
index 00000000000..a82255249ec
--- /dev/null
+++ b/mirage/fixtures/crate_teams.js
@@ -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"
+ }
+ ]
+};
diff --git a/mirage/fixtures/team.js b/mirage/fixtures/team.js
new file mode 100644
index 00000000000..c7c77aa0adc
--- /dev/null
+++ b/mirage/fixtures/team.js
@@ -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",
+ }
+};
diff --git a/mirage/fixtures/user.js b/mirage/fixtures/user.js
new file mode 100644
index 00000000000..f26e1175cf6
--- /dev/null
+++ b/mirage/fixtures/user.js
@@ -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"
+ }
+};
diff --git a/src/krate.rs b/src/krate.rs
index 8c09683bc94..84e0db5ccd4 100644
--- a/src/krate.rs
+++ b/src/krate.rs
@@ -114,6 +114,8 @@ pub struct CrateLinks {
pub version_downloads: String,
pub versions: Option
,
pub owners: Option,
+ pub owner_team: Option,
+ pub owner_user: Option,
pub reverse_dependencies: String,
}
@@ -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),
},
}
@@ -864,6 +868,15 @@ pub fn index(req: &mut Request) -> CargoResult {
.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::().ok()) {
+ 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(
@@ -1423,6 +1436,40 @@ pub fn owners(req: &mut Request) -> CargoResult {
Ok(req.json(&R { users: owners }))
}
+/// Handles the `GET /crates/:crate_id/owner_team` route.
+pub fn owner_team(req: &mut Request) -> CargoResult {
+ let crate_name = &req.params()["crate_id"];
+ let conn = req.db_conn()?;
+ let krate = Crate::by_name(crate_name).first::(&*conn)?;
+ let owners = Team::owning(&krate, &conn)?
+ .into_iter()
+ .map(Owner::encodable)
+ .collect();
+
+ #[derive(RustcEncodable)]
+ struct R {
+ teams: Vec,
+ }
+ Ok(req.json(&R { teams: owners }))
+}
+
+/// Handles the `GET /crates/:crate_id/owner_user` route.
+pub fn owner_user(req: &mut Request) -> CargoResult {
+ let crate_name = &req.params()["crate_id"];
+ let conn = req.db_conn()?;
+ let krate = Crate::by_name(crate_name).first::(&*conn)?;
+ let owners = User::owning(&krate, &conn)?
+ .into_iter()
+ .map(Owner::encodable)
+ .collect();
+
+ #[derive(RustcEncodable)]
+ struct R {
+ users: Vec,
+ }
+ Ok(req.json(&R { users: owners }))
+}
+
/// Handles the `PUT /crates/:crate_id/owners` route.
pub fn add_owners(req: &mut Request) -> CargoResult {
modify_owners(req, true)
diff --git a/src/lib.rs b/src/lib.rs
index db1d485e6c7..55388b830cc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -129,6 +129,8 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
api_router.delete("/crates/:crate_id/follow", C(krate::unfollow));
api_router.get("/crates/:crate_id/following", C(krate::following));
api_router.get("/crates/:crate_id/owners", C(krate::owners));
+ api_router.get("/crates/:crate_id/owner_team", C(krate::owner_team));
+ api_router.get("/crates/:crate_id/owner_user", C(krate::owner_user));
api_router.put("/crates/:crate_id/owners", C(krate::add_owners));
api_router.delete("/crates/:crate_id/owners", C(krate::remove_owners));
api_router.delete("/crates/:crate_id/:version/yank", C(version::yank));
@@ -145,6 +147,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
api_router.get("/categories/:category_id", C(category::show));
api_router.get("/category_slugs", C(category::slugs));
api_router.get("/users/:user_id", C(user::show));
+ api_router.get("/teams/:team_id", C(user::show_team));
let api_router = Arc::new(R404(api_router));
let mut router = RouteBuilder::new();
diff --git a/src/owner.rs b/src/owner.rs
index 407527055c2..9b91e9b6af7 100644
--- a/src/owner.rs
+++ b/src/owner.rs
@@ -1,4 +1,3 @@
-use diesel;
use diesel::prelude::*;
use diesel::pg::PgConnection;
use pg::rows::Row;
@@ -36,7 +35,7 @@ pub enum Owner {
/// For now, just a Github Team. Can be upgraded to other teams
/// later if desirable.
-#[derive(Queryable, Identifiable)]
+#[derive(Queryable, Identifiable, RustcEncodable, RustcDecodable)]
pub struct Team {
/// Unique table id
pub id: i32,
@@ -53,6 +52,15 @@ pub struct Team {
}
#[derive(RustcEncodable)]
+pub struct EncodableTeam {
+ pub id: i32,
+ pub login: String,
+ pub name: Option,
+ pub avatar: Option,
+ pub url: Option,
+}
+
+#[derive(RustcEncodable, RustcDecodable)]
pub struct EncodableOwner {
pub id: i32,
pub login: String,
@@ -72,6 +80,41 @@ pub enum Rights {
Full,
}
+#[derive(Insertable, AsChangeset)]
+#[table_name = "teams"]
+pub struct NewTeam<'a> {
+ pub login: &'a str,
+ pub github_id: i32,
+ pub name: Option,
+ pub avatar: Option,
+}
+
+impl<'a> NewTeam<'a> {
+ pub fn new(
+ login: &'a str,
+ github_id: i32,
+ name: Option,
+ avatar: Option,
+ ) -> Self {
+ NewTeam {
+ login: login,
+ github_id: github_id,
+ name: name,
+ avatar: avatar,
+ }
+ }
+
+ pub fn create_or_update(&self, conn: &PgConnection) -> CargoResult {
+ use diesel::insert;
+ use diesel::pg::upsert::*;
+
+ insert(&self.on_conflict(teams::github_id, do_update().set(self)))
+ .into(teams::table)
+ .get_result(conn)
+ .map_err(Into::into)
+ }
+}
+
impl Team {
/// Tries to create the Team in the DB (assumes a `:` has already been found).
pub fn create(
@@ -172,39 +215,7 @@ impl Team {
let (handle, resp) = http::github(app, &url, &token)?;
let org: Org = http::parse_github_response(handle, &resp)?;
- Team::insert(conn, login, team.id, team.name, org.avatar_url)
- }
-
- pub fn insert(
- conn: &PgConnection,
- login: &str,
- github_id: i32,
- name: Option,
- avatar: Option,
- ) -> CargoResult {
- use diesel::pg::upsert::*;
-
- #[derive(Insertable, AsChangeset)]
- #[table_name = "teams"]
- struct NewTeam<'a> {
- login: &'a str,
- github_id: i32,
- name: Option,
- avatar: Option,
- }
- let new_team = NewTeam {
- login: login,
- github_id: github_id,
- name: name,
- avatar: avatar,
- };
-
- diesel::insert(&new_team.on_conflict(
- teams::github_id,
- do_update().set(&new_team),
- )).into(teams::table)
- .get_result(conn)
- .map_err(Into::into)
+ NewTeam::new(login, team.id, team.name, org.avatar_url).create_or_update(conn)
}
/// Phones home to Github to ask if this User is a member of the given team.
@@ -214,6 +225,49 @@ impl Team {
pub fn contains_user(&self, app: &App, user: &User) -> CargoResult {
team_with_gh_id_contains_user(app, self.github_id, user)
}
+
+ pub fn owning(krate: &Crate, conn: &PgConnection) -> CargoResult> {
+ let base_query = CrateOwner::belonging_to(krate).filter(crate_owners::deleted.eq(false));
+ let teams = base_query
+ .inner_join(teams::table)
+ .select(teams::all_columns)
+ .filter(crate_owners::owner_kind.eq(OwnerKind::Team as i32))
+ .load(conn)?
+ .into_iter()
+ .map(Owner::Team);
+
+ Ok(teams.collect())
+ }
+
+ pub fn encodable(self) -> EncodableTeam {
+ let Team {
+ id,
+ name,
+ login,
+ avatar,
+ ..
+ } = self;
+ let url = Team::github_url(&login);
+
+ EncodableTeam {
+ id: id,
+ login: login,
+ name: name,
+ avatar: avatar,
+ url: Some(url),
+ }
+ }
+
+ fn github_url(login: &str) -> String {
+ let mut login_pieces = login.split(':');
+ login_pieces.next();
+
+ format!(
+ "https://github.com/orgs/{}/teams/{}",
+ login_pieces.next().expect("org failed"),
+ login_pieces.next().expect("team failed")
+ )
+ }
}
fn team_with_gh_id_contains_user(app: &App, github_id: i32, user: &User) -> CargoResult {
@@ -330,15 +384,7 @@ impl Owner {
avatar,
..
}) => {
- let url = {
- let mut parts = login.split(':');
- parts.next(); // discard github
- format!(
- "https://github.com/orgs/{}/teams/{}",
- parts.next().unwrap(),
- parts.next().unwrap()
- )
- };
+ let url = Team::github_url(&login);
EncodableOwner {
id: id,
login: login,
diff --git a/src/tests/all.rs b/src/tests/all.rs
index 61b4255a5e7..043f3ea43f9 100644
--- a/src/tests/all.rs
+++ b/src/tests/all.rs
@@ -31,12 +31,15 @@ use cargo_registry::keyword::Keyword;
use cargo_registry::krate::NewCrate;
use cargo_registry::upload as u;
use cargo_registry::user::NewUser;
+use cargo_registry::owner::{CrateOwner, NewTeam, Team};
use cargo_registry::version::NewVersion;
use cargo_registry::{User, Crate, Version, Dependency, Category, Model, Replica};
use conduit::{Request, Method};
use conduit_test::MockRequest;
use diesel::pg::PgConnection;
use diesel::prelude::*;
+use diesel::pg::upsert::*;
+use cargo_registry::schema::*;
macro_rules! t {
($e:expr) => (
@@ -212,6 +215,32 @@ fn user(login: &str) -> User {
}
}
+fn new_team(login: &str) -> NewTeam {
+ NewTeam {
+ github_id: NEXT_ID.fetch_add(1, Ordering::SeqCst) as i32,
+ login: login,
+ name: None,
+ avatar: None,
+ }
+}
+
+fn add_team_to_crate(t: &Team, krate: &Crate, u: &User, conn: &PgConnection) -> CargoResult<()> {
+ let crate_owner = CrateOwner {
+ crate_id: krate.id,
+ owner_id: t.id,
+ created_by: u.id,
+ owner_kind: 1, // Team owner kind is 1 according to owner.rs
+ };
+
+ diesel::insert(&crate_owner.on_conflict(
+ crate_owners::table.primary_key(),
+ do_update().set(crate_owners::deleted.eq(false)),
+ )).into(crate_owners::table)
+ .execute(conn)?;
+
+ Ok(())
+}
+
use cargo_registry::util::CargoResult;
struct CrateBuilder<'a> {
diff --git a/src/tests/krate.rs b/src/tests/krate.rs
index 6875d7b478b..1c6df816eff 100644
--- a/src/tests/krate.rs
+++ b/src/tests/krate.rs
@@ -18,6 +18,7 @@ use cargo_registry::upload as u;
use cargo_registry::user::EncodableUser;
use cargo_registry::version::EncodableVersion;
use cargo_registry::category::Category;
+use cargo_registry::owner::EncodableOwner;
#[derive(RustcDecodable)]
struct CrateList {
@@ -61,6 +62,14 @@ struct RevDeps {
struct Downloads {
version_downloads: Vec,
}
+#[derive(RustcDecodable)]
+struct TeamResponse {
+ teams: Vec,
+}
+#[derive(RustcDecodable)]
+struct UserResponse {
+ users: Vec,
+}
fn new_crate(name: &str) -> u::NewCrate {
u::NewCrate {
@@ -1740,3 +1749,92 @@ fn author_license_and_description_required() {
json.errors
);
}
+
+/* Testing the crate ownership between two crates and one team.
+ Given two crates, one crate owned by both a team and a user,
+ one only owned by a user, check that the CrateList returned
+ for the user_id contains only the crates owned by that user,
+ and that the CrateList returned for the team_id contains
+ only crates owned by that team.
+*/
+#[test]
+fn check_ownership_two_crates() {
+ let (_b, app, middle) = ::app();
+
+ let (krate_owned_by_team, team) = {
+ let conn = app.diesel_database.get().unwrap();
+ let u = ::new_user("user_foo").create_or_update(&conn).unwrap();
+ let t = ::new_team("team_foo").create_or_update(&conn).unwrap();
+ let krate = ::CrateBuilder::new("foo", u.id).expect_build(&conn);
+ ::add_team_to_crate(&t, &krate, &u, &conn).unwrap();
+ (krate, t)
+ };
+
+ let (krate_not_owned_by_team, user) = {
+ let conn = app.diesel_database.get().unwrap();
+ let u = ::new_user("user_bar").create_or_update(&conn).unwrap();
+ (::CrateBuilder::new("bar", u.id).expect_build(&conn), u)
+ };
+
+ let mut req = ::req(app.clone(), Method::Get, "/api/v1/crates");
+
+ let query = format!("user_id={}", user.id);
+ let mut response = ok_resp!(middle.call(req.with_query(&query)));
+ let json: CrateList = ::json(&mut response);
+
+ assert_eq!(json.crates[0].name, krate_not_owned_by_team.name);
+ assert_eq!(json.crates.len(), 1);
+
+ let query = format!("team_id={}", team.id);
+ let mut response = ok_resp!(middle.call(req.with_query(&query)));
+
+ let json: CrateList = ::json(&mut response);
+ assert_eq!(json.crates.len(), 1);
+ assert_eq!(json.crates[0].name, krate_owned_by_team.name);
+}
+
+/* Given a crate owned by both a team and a user, check that the
+ JSON returned by the /owner_team route and /owner_user route
+ contains the correct kind of owner
+
+ Note that in this case function new_team must take a team name
+ of form github:org_name:team_name as that is the format
+ EncodableOwner::encodable is expecting
+*/
+#[test]
+fn check_ownership_one_crate() {
+ let (_b, app, middle) = ::app();
+
+ let (team, user) = {
+ let conn = app.diesel_database.get().unwrap();
+ let u = ::new_user("user_cat").create_or_update(&conn).unwrap();
+ let t = ::new_team("github:test_org:team_sloth")
+ .create_or_update(&conn)
+ .unwrap();
+ let krate = ::CrateBuilder::new("best_crate", u.id).expect_build(&conn);
+ ::add_team_to_crate(&t, &krate, &u, &conn).unwrap();
+ (t, u)
+ };
+
+ let mut req = ::req(
+ app.clone(),
+ Method::Get,
+ "/api/v1/crates/best_crate/owner_team",
+ );
+ let mut response = ok_resp!(middle.call(&mut req));
+ let json: TeamResponse = ::json(&mut response);
+
+ assert_eq!(json.teams[0].kind, "team");
+ assert_eq!(json.teams[0].name, team.name);
+
+ let mut req = ::req(
+ app.clone(),
+ Method::Get,
+ "/api/v1/crates/best_crate/owner_user",
+ );
+ let mut response = ok_resp!(middle.call(&mut req));
+ let json: UserResponse = ::json(&mut response);
+
+ assert_eq!(json.users[0].kind, "user");
+ assert_eq!(json.users[0].name, user.name);
+}
diff --git a/src/user/mod.rs b/src/user/mod.rs
index d587577538f..88202918653 100644
--- a/src/user/mod.rs
+++ b/src/user/mod.rs
@@ -16,6 +16,8 @@ use util::errors::NotFound;
use util::{RequestUtils, CargoResult, internal, ChainError, human};
use version::EncodableVersion;
use {http, Model, Version};
+use owner::{Owner, OwnerKind, CrateOwner};
+use krate::Crate;
pub use self::middleware::{Middleware, RequestUser};
@@ -160,6 +162,19 @@ impl User {
})?))
}
+ pub fn owning(krate: &Crate, conn: &PgConnection) -> CargoResult> {
+ let base_query = CrateOwner::belonging_to(krate).filter(crate_owners::deleted.eq(false));
+ let users = base_query
+ .inner_join(users::table)
+ .select(users::all_columns)
+ .filter(crate_owners::owner_kind.eq(OwnerKind::User as i32))
+ .load(conn)?
+ .into_iter()
+ .map(Owner::User);
+
+ Ok(users.collect())
+ }
+
/// Converts this `User` model into an `EncodableUser` for JSON serialization.
pub fn encodable(self) -> EncodableUser {
let User {
@@ -373,6 +388,22 @@ pub fn show(req: &mut Request) -> CargoResult {
Ok(req.json(&R { user: user.encodable() }))
}
+/// Handles the `GET /teams/:team_id` route.
+pub fn show_team(req: &mut Request) -> CargoResult {
+ use self::teams::dsl::{teams, login};
+ use owner::Team;
+ use owner::EncodableTeam;
+
+ let name = &req.params()["team_id"];
+ let conn = req.db_conn()?;
+ let team = teams.filter(login.eq(name)).first::(&*conn)?;
+
+ #[derive(RustcEncodable)]
+ struct R {
+ team: EncodableTeam,
+ }
+ Ok(req.json(&R { team: team.encodable() }))
+}
/// Handles the `GET /me/updates` route.
pub fn updates(req: &mut Request) -> CargoResult {
diff --git a/tests/acceptance/crate-test.js b/tests/acceptance/crate-test.js
index 47bad3f728c..7aa540b2673 100644
--- a/tests/acceptance/crate-test.js
+++ b/tests/acceptance/crate-test.js
@@ -43,3 +43,41 @@ test('navigating to the reverse dependencies page', function(assert) {
hasText(assert, $revDep, 'unicorn-rpc');
});
});
+
+test('navigating to a user page', function(assert) {
+ visit('/crates/nanomsg');
+ click('.owners li:last a');
+
+ andThen(function() {
+ assert.equal(currentURL(), '/users/blabaere');
+ hasText(assert, '#crates-heading h1', 'thehydroimpulse');
+ });
+});
+
+test('navigating to a team page', function(assert) {
+ visit('/crates/nanomsg');
+ click('.owners li:first a ');
+
+ andThen(function() {
+ assert.equal(currentURL(), '/teams/github:org:thehydroimpulse');
+ hasText(assert, '.team-info h2', 'thehydroimpulseteam');
+ });
+});
+
+test('crates having user-owners', function(assert) {
+ visit('/crates/nanomsg');
+
+ andThen(function() {
+ findWithAssert('ul.owners li:first a[href="/teams/github:org:thehydroimpulse"] img[src="https://avatars.githubusercontent.com/u/565790?v=3&s=64"]');
+ assert.equal(find('ul.owners li').length, 4);
+ });
+});
+
+test('crates having team-owners', function(assert) {
+ visit('/crates/nanomsg');
+
+ andThen(function() {
+ findWithAssert('ul.owners li:first a[href="/teams/github:org:thehydroimpulse"]');
+ assert.equal(find('ul.owners li').length, 4);
+ });
+});
diff --git a/tests/acceptance/team-page-test.js b/tests/acceptance/team-page-test.js
new file mode 100644
index 00000000000..9e2a164fa0c
--- /dev/null
+++ b/tests/acceptance/team-page-test.js
@@ -0,0 +1,41 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | team page');
+
+test('has team organization display', function(assert) {
+ visit('/teams/github:org:thehydroimpulse');
+
+ andThen(function() {
+ hasText(assert, '.team-info h1', 'org_test');
+ hasText(assert, '.team-info h2', 'thehydroimpulseteam');
+ });
+});
+
+test('has link to github in team header', function(assert) {
+ visit('/teams/github:org:thehydroimpulse');
+
+ andThen(function() {
+ const $githubLink = findWithAssert('.info a');
+ assert.equal($githubLink.attr('href').trim(), 'https://github.com/thehydroimpulse');
+ });
+
+});
+
+test('github link has image in team header', function(assert) {
+ visit('/teams/github:org:thehydroimpulse');
+
+ andThen(function() {
+ const $githubImg = findWithAssert('.info a img');
+ assert.equal($githubImg.attr('src').trim(), '/assets/GitHub-Mark-32px.png');
+ });
+});
+
+test('team organization details has github profile icon', function(assert) {
+ visit('/teams/github:org:thehydroimpulse');
+
+ andThen(function() {
+ const $githubProfileImg = findWithAssert('.info img');
+ assert.equal($githubProfileImg.attr('src').trim(), 'https://avatars.githubusercontent.com/u/565790?v=3&s=170');
+ });
+});
diff --git a/tests/acceptance/user-page-test.js b/tests/acceptance/user-page-test.js
new file mode 100644
index 00000000000..4d8ea96247e
--- /dev/null
+++ b/tests/acceptance/user-page-test.js
@@ -0,0 +1,39 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | user page');
+
+test('has user display', function(assert) {
+ visit('/users/thehydroimpulse');
+
+ andThen(function() {
+ hasText(assert, '#crates-heading h1', 'thehydroimpulse');
+ });
+});
+
+test('has link to github in user header', function(assert) {
+ visit('/users/thehydroimpulse');
+
+ andThen(function() {
+ const $githubLink = findWithAssert('#crates-heading a');
+ assert.equal($githubLink.attr('href').trim(), 'https://github.com/thehydroimpulse');
+ });
+});
+
+test('github link has image in user header', function(assert) {
+ visit('/users/thehydroimpulse');
+
+ andThen(function() {
+ const $githubImg = findWithAssert('#crates-heading a img');
+ assert.equal($githubImg.attr('src').trim(), '/assets/GitHub-Mark-32px.png');
+ });
+});
+
+test('user details has github profile icon', function(assert) {
+ visit('/users/thehydroimpulse');
+
+ andThen(function() {
+ const $githubProfileImg = findWithAssert('#crates-heading img');
+ assert.equal($githubProfileImg.attr('src').trim(), 'https://avatars.githubusercontent.com/u/565790?v=3&s=170');
+ });
+});