From 54fe9719721797f6c9f16c10b74b923bea8189c6 Mon Sep 17 00:00:00 2001 From: PritiKumr Date: Wed, 14 Jun 2017 18:17:23 +0530 Subject: [PATCH 01/11] #494 WIP - Option to favorite and unfavorite user --- app/adapters/user.js | 12 ++++ app/controllers/user.js | 16 ++++++ app/models/user.js | 8 +++ app/routes/user.js | 13 +++++ app/templates/user.hbs | 33 ++++++++--- .../down.sql | 1 + .../up.sql | 9 +++ .../down.sql | 1 + .../up.sql | 1 + src/lib.rs | 3 + src/schema.rs | 8 +++ src/user/mod.rs | 57 +++++++++++++++++++ 12 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 migrations/20170613160003_create_favorite_users/down.sql create mode 100644 migrations/20170613160003_create_favorite_users/up.sql create mode 100644 migrations/20170614104258_create_index_on_favorite_users/down.sql create mode 100644 migrations/20170614104258_create_index_on_favorite_users/up.sql diff --git a/app/adapters/user.js b/app/adapters/user.js index ba343fc4cfd..2bf5f2e4dd7 100644 --- a/app/adapters/user.js +++ b/app/adapters/user.js @@ -13,4 +13,16 @@ export default ApplicationAdapter.extend({ let url = this.urlForFindRecord(query.user_id, 'user'); return this.ajax(url, 'GET'); }, + + favorite(id) { + return this.ajax(this.urlForFavoriteAction(id), 'PUT'); + }, + + unfavorite(id) { + return this.ajax(this.urlForFavoriteAction(id), 'DELETE'); + }, + + urlForFavoriteAction(id) { + return `${this.buildURL('user', id)}/favorite`; + }, }); diff --git a/app/controllers/user.js b/app/controllers/user.js index ab3088fd4e9..5dc0974fb1e 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -21,4 +21,20 @@ export default Controller.extend(PaginationMixin, { return 'Alphabetical'; } }), + + fetchingFavorite: false, + favorited: false, + + actions: { + toggleFavorite() { + this.set('fetchingFavorite', true); + + let owner = this.get('user'); + let op = this.toggleProperty('favorited') ? + owner.favorite() : owner.unfavorite(); + + return op.finally(() => this.set('fetchingFavorite', false)); + }, + }, + }); diff --git a/app/models/user.js b/app/models/user.js index a5348032596..677e515e2f2 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -11,4 +11,12 @@ export default DS.Model.extend({ stats() { return this.store.adapterFor('user').stats(this.get('id')); }, + + favorite() { + return this.store.adapterFor('user').favorite(this.get('id')); + }, + + unfavorite() { + return this.store.adapterFor('user').unfavorite(this.get('id')); + }, }); diff --git a/app/routes/user.js b/app/routes/user.js index d360b7db977..052100bcb65 100644 --- a/app/routes/user.js +++ b/app/routes/user.js @@ -1,6 +1,8 @@ +<<<<<<< 120f8008fb08c21fc6cd239c1100692a0ff487e6 import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import RSVP from 'rsvp'; +import ajax from 'ic-ajax'; export default Route.extend({ flashMessages: service(), @@ -36,5 +38,16 @@ export default Route.extend({ controller.set('fetchingFeed', true); controller.set('crates', this.get('data.crates')); + controller.set('user', model.user); + controller.set( + 'allowFavorting', + this.session.get('currentUser') != model.user + ); + + if (controller.get('allowFavorting')) { + ajax(`/api/v1/users/${model.user.id}/favorited`) + .then((d) => controller.set('favorited', d.favorited)) + .finally(() => controller.set('fetchingFavorite', false)); + } }, }); diff --git a/app/templates/user.hbs b/app/templates/user.hbs index 3602d54277f..1b7651767f4 100644 --- a/app/templates/user.hbs +++ b/app/templates/user.hbs @@ -1,11 +1,30 @@
- {{user-avatar user=model.user size='medium'}} -

- {{ model.user.login }} -

- {{#user-link user=model.user}} - GitHub profile - {{/user-link}} +
+
+ {{user-avatar user=model.user size='medium'}} +

+ {{ model.user.login }} +

+ {{#user-link user=model.user}} + GitHub profile + {{/user-link}} +
+
+ {{#if allowFavorting}} + + {{/if}} +
+
diff --git a/migrations/20170613160003_create_favorite_users/down.sql b/migrations/20170613160003_create_favorite_users/down.sql new file mode 100644 index 00000000000..08b7ca5d399 --- /dev/null +++ b/migrations/20170613160003_create_favorite_users/down.sql @@ -0,0 +1 @@ +DROP TABLE favorite_users; \ No newline at end of file diff --git a/migrations/20170613160003_create_favorite_users/up.sql b/migrations/20170613160003_create_favorite_users/up.sql new file mode 100644 index 00000000000..fdf318b29ba --- /dev/null +++ b/migrations/20170613160003_create_favorite_users/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE favorite_users ( + user_id INTEGER NOT NULL, + target_id INTEGER NOT NULL, + CONSTRAINT favorites_pkey PRIMARY KEY (user_id, target_id), + CONSTRAINT fk_favorites_user_id FOREIGN KEY (user_id) + REFERENCES users (id), + CONSTRAINT fk_favorites_target_id FOREIGN KEY (target_id) + REFERENCES users (id) + ); \ No newline at end of file diff --git a/migrations/20170614104258_create_index_on_favorite_users/down.sql b/migrations/20170614104258_create_index_on_favorite_users/down.sql new file mode 100644 index 00000000000..b2a5bed9118 --- /dev/null +++ b/migrations/20170614104258_create_index_on_favorite_users/down.sql @@ -0,0 +1 @@ +DROP INDEX index_favorites_user_id; \ No newline at end of file diff --git a/migrations/20170614104258_create_index_on_favorite_users/up.sql b/migrations/20170614104258_create_index_on_favorite_users/up.sql new file mode 100644 index 00000000000..3c27c8afc13 --- /dev/null +++ b/migrations/20170614104258_create_index_on_favorite_users/up.sql @@ -0,0 +1 @@ +CREATE INDEX index_favorites_user_id ON favorite_users (user_id); \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bf446745b96..6d334de664c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,6 +153,9 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { api_router.get("/users/:user_id", C(user::show)); api_router.get("/users/:user_id/stats", C(user::stats)); api_router.get("/teams/:team_id", C(user::show_team)); + api_router.get("/users/:user_id/favorited", C(user::favorited)); + api_router.put("/users/:user_id/favorite", C(user::favorite)); + api_router.delete("/users/:user_id/favorite", C(user::unfavorite)); let api_router = Arc::new(R404(api_router)); let mut router = RouteBuilder::new(); diff --git a/src/schema.rs b/src/schema.rs index 2f25b813460..ee791f4dce4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -183,3 +183,11 @@ table! { license -> Nullable, } } + +table! { + favorite_users (user_id, + target_id) { + user_id -> Int4, + target_id -> Int4, + } +} \ No newline at end of file diff --git a/src/user/mod.rs b/src/user/mod.rs index da1e8c17a63..e16fcb6c1e6 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -2,6 +2,8 @@ use conduit::{Request, Response}; use conduit_cookie::RequestSession; use conduit_router::RequestParams; use diesel::prelude::*; +use diesel; + use diesel::pg::PgConnection; use pg::GenericConnection; use pg::rows::Row; @@ -375,6 +377,61 @@ pub fn show_team(req: &mut Request) -> CargoResult { Ok(req.json(&R { team: team.encodable() })) } +#[derive(Insertable, Queryable, Identifiable, Associations)] +#[belongs_to(User)] +#[primary_key(user_id, target_id)] +#[table_name="favorite_users"] +pub struct FavoriteUser { + user_id: i32, + target_id: i32, +} + +fn favorite_target(req: &mut Request) -> CargoResult { + let user = req.user()?; + let target_user_id: i32 = req.params()["user_id"].parse().expect("User ID not found"); + Ok(FavoriteUser { + user_id: user.id, + target_id: target_user_id, + }) +} + +/// Handles the `PUT /users/:user_id/favorite` route. +pub fn favorite(req: &mut Request) -> CargoResult { + use diesel::pg::upsert::OnConflictExtension; + + let favorite = favorite_target(req)?; + let conn = req.db_conn()?; + diesel::insert(&favorite.on_conflict_do_nothing()) + .into(favorite_users::table) + .execute(&*conn)?; + #[derive(RustcEncodable)] + struct R { ok: bool } + Ok(req.json(&R { ok: true })) +} + +/// Handles the `DELETE /users/:user_id/favorite` route. +pub fn unfavorite(req: &mut Request) -> CargoResult { + let favorite = favorite_target(req)?; + let conn = req.db_conn()?; + diesel::delete(&favorite).execute(&*conn)?; + #[derive(RustcEncodable)] + struct R { ok: bool } + Ok(req.json(&R { ok: true })) +} + +/// Handles the `GET /users/:user_id/favorited` route. +pub fn favorited(req: &mut Request) -> CargoResult { + use diesel::expression::dsl::exists; + + let fav = favorite_target(req)?; + let conn = req.db_conn()?; + let favorited = diesel::select(exists(favorite_users::table.find(fav.id()))) + .get_result(&*conn)?; + #[derive(RustcEncodable)] + struct R { favorited: bool } + Ok(req.json(&R { favorited: favorited })) +} + /// Handles the `GET /me/updates` route. pub fn updates(req: &mut Request) -> CargoResult { use diesel::expression::dsl::any; From 21210209f54e50e1fd1d8b0fa305b65789774a08 Mon Sep 17 00:00:00 2001 From: PritiKumr Date: Wed, 14 Jun 2017 19:39:38 +0530 Subject: [PATCH 02/11] WIP --- app/adapters/user.js | 4 ++++ app/controllers/dashboard.js | 9 ++++++++ app/models/user.js | 5 +++++ app/routes/dashboard.js | 5 +++++ app/routes/user.js | 4 ++-- app/templates/components/users-list.hbs | 9 ++++++++ app/templates/dashboard.hbs | 9 ++++++++ src/lib.rs | 1 + src/user/mod.rs | 28 ++++++++++++++++++++++++- 9 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 app/templates/components/users-list.hbs diff --git a/app/adapters/user.js b/app/adapters/user.js index 2bf5f2e4dd7..b78b7a92ab1 100644 --- a/app/adapters/user.js +++ b/app/adapters/user.js @@ -25,4 +25,8 @@ export default ApplicationAdapter.extend({ urlForFavoriteAction(id) { return `${this.buildURL('user', id)}/favorite`; }, + + favorite_users(id) { + return this.ajax(`${this.buildURL('user', id)}/favorite_users`, 'GET'); + }, }); diff --git a/app/controllers/dashboard.js b/app/controllers/dashboard.js index d6cdf91f6d7..783527c5490 100644 --- a/app/controllers/dashboard.js +++ b/app/controllers/dashboard.js @@ -18,6 +18,7 @@ export default Controller.extend({ this.myFollowing = []; this.myFeed = []; this.myStats = 0; + this.favoriteUsers = []; }, visibleCrates: computed('myCrates.[]', function() { @@ -36,10 +37,18 @@ export default Controller.extend({ return this.get('myCrates.length') > TO_SHOW; }), + visibleFavorites: computed('favoriteUsers', function() { + return this.get('favoriteUsers').slice(0, TO_SHOW); + }), + hasMoreFollowing: computed('myFollowing.[]', function() { return this.get('myFollowing.length') > TO_SHOW; }), + hasMoreFavorites: computed('favoriteUsers', function() { + return this.get('favoriteUsers.length') > TO_SHOW; + }), + actions: { loadMore() { this.set('loadingMore', true); diff --git a/app/models/user.js b/app/models/user.js index 677e515e2f2..1a70f9abdf0 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -19,4 +19,9 @@ export default DS.Model.extend({ unfavorite() { return this.store.adapterFor('user').unfavorite(this.get('id')); }, + + favorite_users() { + return this.store.adapterFor('user').favorite_users(this.get('id')); + }, + }); diff --git a/app/routes/dashboard.js b/app/routes/dashboard.js index efa37405f1b..fb991f68fc1 100644 --- a/app/routes/dashboard.js +++ b/app/routes/dashboard.js @@ -13,6 +13,7 @@ export default Route.extend(AuthenticatedRoute, { controller.set('myCrates', this.get('data.myCrates')); controller.set('myFollowing', this.get('data.myFollowing')); controller.set('myStats', this.get('data.myStats')); + controller.set('favoriteUsers', this.get('data.favoriteUsers')); if (!controller.get('loadingMore')) { controller.set('myFeed', []); @@ -35,6 +36,10 @@ export default Route.extend(AuthenticatedRoute, { let myStats = user.stats(); + let favoriteUsers = this.store.query('crate', { + following: 1 + }); + return RSVP.hash({ myCrates, myFollowing, diff --git a/app/routes/user.js b/app/routes/user.js index 052100bcb65..1101982dbab 100644 --- a/app/routes/user.js +++ b/app/routes/user.js @@ -40,8 +40,8 @@ export default Route.extend({ controller.set('crates', this.get('data.crates')); controller.set('user', model.user); controller.set( - 'allowFavorting', - this.session.get('currentUser') != model.user + 'allowFavorting', + this.session.get('currentUser') !== model.user ); if (controller.get('allowFavorting')) { diff --git a/app/templates/components/users-list.hbs b/app/templates/components/users-list.hbs new file mode 100644 index 00000000000..c2b268fcf8b --- /dev/null +++ b/app/templates/components/users-list.hbs @@ -0,0 +1,9 @@ +
    + {{#each users as |user|}} +
  • + {{#link-to 'user' user.id class='name'}} + {{ user.name }} + {{/link-to}} +
  • + {{/each}} +
diff --git a/app/templates/dashboard.hbs b/app/templates/dashboard.hbs index 6d3fc848db9..ab91fe4f0fa 100644 --- a/app/templates/dashboard.hbs +++ b/app/templates/dashboard.hbs @@ -45,6 +45,15 @@
{{crate-downloads-list crates=visibleFollowing}} +
+
+

+ + Favorite Users +

+
+ {{users-list users=favoriteUsers}} +
diff --git a/src/lib.rs b/src/lib.rs index 6d334de664c..962c44863e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,6 +156,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { api_router.get("/users/:user_id/favorited", C(user::favorited)); api_router.put("/users/:user_id/favorite", C(user::favorite)); api_router.delete("/users/:user_id/favorite", C(user::unfavorite)); + api_router.get("/users/:user_id/favorite_users", C(user::favorite_users)); let api_router = Arc::new(R404(api_router)); let mut router = RouteBuilder::new(); diff --git a/src/user/mod.rs b/src/user/mod.rs index e16fcb6c1e6..e1fafb5801e 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -27,7 +27,8 @@ pub use self::middleware::{Middleware, RequestUser, AuthenticationSource}; pub mod middleware; /// The model representing a row in the `users` database table. -#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable)] +#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, Associations)] +#[has_many(favorite_users)] pub struct User { pub id: i32, pub email: Option, @@ -432,6 +433,31 @@ pub fn favorited(req: &mut Request) -> CargoResult { Ok(req.json(&R { favorited: favorited })) } + + + + +/// Handles the `GET /users/:user_id/favorite_users` route. +pub fn favorite_users(req: &mut Request) -> CargoResult { + let user_id: i32 = req.params()["user_id"].parse() + .expect("User ID not found"); + let conn = req.db_conn()?; + + let users = users::table.filter(users::id.eq_any( + favorite_users::table.select(favorite_users::target_id) + .filter(favorite_users::user_id.eq(user_id)) + )).select(users::all_columns) + .load::(&*conn)? + .into_iter().map(|u| u.encodable()).collect(); + + #[derive(RustcEncodable)] + struct R { users: Vec } + Ok(req.json(&R{ users: users })) +} + + + + /// Handles the `GET /me/updates` route. pub fn updates(req: &mut Request) -> CargoResult { use diesel::expression::dsl::any; From a31aa6f77d98e00bc00a14cf0699379d0d8bac90 Mon Sep 17 00:00:00 2001 From: PritiKumr Date: Tue, 20 Jun 2017 18:41:59 +0530 Subject: [PATCH 03/11] favorite users list in dashboard and unfavorite option --- app/adapters/user.js | 2 +- app/controllers/dashboard.js | 8 +- app/models/user.js | 4 +- app/routes/dashboard.js | 8 +- app/styles/app.scss | 83 ++++++++++++++++++++ app/styles/me.scss | 25 +++++- app/templates/components/favorite-users.hbs | 16 ++++ app/templates/components/users-list.hbs | 9 --- app/templates/dashboard.hbs | 4 +- public/assets/delete.png | Bin 0 -> 1514 bytes public/assets/favorite.png | Bin 0 -> 2343 bytes src/user/mod.rs | 1 + 12 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 app/templates/components/favorite-users.hbs delete mode 100644 app/templates/components/users-list.hbs create mode 100644 public/assets/delete.png create mode 100644 public/assets/favorite.png diff --git a/app/adapters/user.js b/app/adapters/user.js index b78b7a92ab1..96ea3664979 100644 --- a/app/adapters/user.js +++ b/app/adapters/user.js @@ -26,7 +26,7 @@ export default ApplicationAdapter.extend({ return `${this.buildURL('user', id)}/favorite`; }, - favorite_users(id) { + favoriteUsers(id) { return this.ajax(`${this.buildURL('user', id)}/favorite_users`, 'GET'); }, }); diff --git a/app/controllers/dashboard.js b/app/controllers/dashboard.js index 783527c5490..8f377280390 100644 --- a/app/controllers/dashboard.js +++ b/app/controllers/dashboard.js @@ -50,7 +50,7 @@ export default Controller.extend({ }), actions: { - loadMore() { + loadMore: function() { this.set('loadingMore', true); let page = (this.get('myFeed').length / 10) + 1; @@ -63,6 +63,12 @@ export default Controller.extend({ }).finally(() => { this.set('loadingMore', false); }); + }, + + unfavoriteUser: function(user) { + this.store.adapterFor('user').unfavorite(user.id).then(() => { + this.get('favoriteUsers').users.removeObject(user); + }); } } }); diff --git a/app/models/user.js b/app/models/user.js index 1a70f9abdf0..1cadf37ecee 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -20,8 +20,8 @@ export default DS.Model.extend({ return this.store.adapterFor('user').unfavorite(this.get('id')); }, - favorite_users() { - return this.store.adapterFor('user').favorite_users(this.get('id')); + favoriteUsers() { + return this.store.adapterFor('user').favoriteUsers(this.get('id')); }, }); diff --git a/app/routes/dashboard.js b/app/routes/dashboard.js index fb991f68fc1..4ce4cfddab9 100644 --- a/app/routes/dashboard.js +++ b/app/routes/dashboard.js @@ -34,16 +34,12 @@ export default Route.extend(AuthenticatedRoute, { following: 1 }); - let myStats = user.stats(); - - let favoriteUsers = this.store.query('crate', { - following: 1 - }); + let favoriteUsers = user.favoriteUsers(); return RSVP.hash({ myCrates, myFollowing, - myStats + favoriteUsers }).then((hash) => { this.set('data', hash); }); diff --git a/app/styles/app.scss b/app/styles/app.scss index 9d3ab9d98ad..c5a8c79665b 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -342,3 +342,86 @@ h1 { padding: 5px; } } + + +img { + &.right-pag, &.left-pag { + width: 29px; + height: 29px; + } + + &.right-arrow, &.right-arrow-all-versions { + width: 20px; + height: 20px; + } + + &.package, &.crate { + width: 32px; + height: 33px; + } + + &.my-packages { + width:15px; + height: 17px; + } + + &.sort { + width: 11px; + height: 12px; + } + + &.flag { + width: 13px; + height: 15px; + } + + &.dashboard { + width: 33px; + height: 33px; + } + + &.lock { + width: 10px; + height: 13px; + } + + &.download-clear-back { + width: 14px; + height: 17px; + } + + &.button-download { + width: 14px; + height: 17px; + } + + &.download { + width: 32px; + height: 33px; + } + + &.following { + width: 15px; + height: 15px; + } + + &.favorite { + width: 20px; + height: 20px; + } + + &.gear { + width: 32px; + height: 32px; + } + + &.circle-with-i { + width: 32px; + height: 32px; + } + + &.latest-updates { + width: 14px; + height: 14px; + } +} diff --git a/app/styles/me.scss b/app/styles/me.scss index 3b0d46371b0..47d8e109b9c 100644 --- a/app/styles/me.scss +++ b/app/styles/me.scss @@ -52,7 +52,7 @@ @include order(1); margin-right: 0; } - #my-crates, #my-following { margin: 0; } + #my-crates, #my-following, #my-favorite-users { margin: 0; } } } #my-feed { @@ -164,3 +164,26 @@ padding: 0px 10px 10px 20px; } } + +#my-favorite-users { + .users { + .user-item { + padding: 13px 10px; + .user-name { + margin-left: 5px; + } + .user-options { + padding-left: 5px; + display: none; + .unfavorite-user { + width: 10px; + height: 10px; + } + } + + &:hover .user-options{ + display: block; + } + } + } +} diff --git a/app/templates/components/favorite-users.hbs b/app/templates/components/favorite-users.hbs new file mode 100644 index 00000000000..26d6ecbf274 --- /dev/null +++ b/app/templates/components/favorite-users.hbs @@ -0,0 +1,16 @@ +
    + {{#each users as |user|}} +
  • + {{#link-to 'user' user.login class="user-item"}} + + {{user-avatar user=user size='medium-small'}} + {{user.name}} + + +
    + +
    + {{/link-to}} +
  • + {{/each}} +
diff --git a/app/templates/components/users-list.hbs b/app/templates/components/users-list.hbs deleted file mode 100644 index c2b268fcf8b..00000000000 --- a/app/templates/components/users-list.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
    - {{#each users as |user|}} -
  • - {{#link-to 'user' user.id class='name'}} - {{ user.name }} - {{/link-to}} -
  • - {{/each}} -
diff --git a/app/templates/dashboard.hbs b/app/templates/dashboard.hbs index ab91fe4f0fa..f1921abbd95 100644 --- a/app/templates/dashboard.hbs +++ b/app/templates/dashboard.hbs @@ -48,11 +48,11 @@

- + Favorite Users

- {{users-list users=favoriteUsers}} + {{favorite-users users=favoriteUsers.users unfavoriteUser=(action "unfavoriteUser")}}
diff --git a/public/assets/delete.png b/public/assets/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..dae3af91637b2c1e2d4260b17d5906b685108e39 GIT binary patch literal 1514 zcmVr}_03UE!kP}Ab zgH>JiKky&YS{a^ajCn)RCBVbL0LVqqw)8;yqvyZd@z)(&x$`C-2(MN61Y~NCxHiIj zS9>eAZ92bcU_2ApH-De+!AU(~l&yp3I0WGu$g5VO;V5^E;(^G^nIv*J4W$!UV6)VxOU)mN#AF+X#)lq+Qs;_r7s{6-pa*jFF*K=z{t|Y)g9amK1!RRiOUEa>k86v#@ zeWG`3BaL=mJEs9-!RQuL_mbx>dW?#&eRU!faboOBS>yib8WlZ++qU05{VKYlCb8|X zQwP^Ag7?BXf+0fZl!AI4&xP|YBZST=1vhQ2mEo#vGM=HfF0VPe?RX)+1ufj{9r3&@ zi;+(Y-Eq23B<^ooEQSQ$|EwUhBCV~#u&Y+zaU!u` zbc>1}Vf;%kK$)m~77s>3P8~|dGEh{W2u^N93FFO<&M5*cSvPCxv`2pncAVz4j%A#a zQwtkQulG;fhsZ`xr|6zTeLcM!hwnHCbI)|Y_wIh=|GGmQQngwu!}E;j8P5h4E&vNi zr2>MuZaZ=?>QU{#iFhw`a?&?2DQG1_SFCe8yC;;(Gk~i`Fb^Yy(?F(gQZdK85;{Q+ z58u&Mb=2SN>*=i##Bpmr2IY#hw)vyW03;TO%mlsYnOZA*R(Cbj3o>fmS5)Hu=xzkZ z$E$e}uB9aPm=Pi5Q4)$!XLW_@awU-&SJdhX)gjaYc}33QsP~oHr42@v>Y{^XjxN_* ziO?RW4q4mvS>Ix*HWa_OZ;&~EJ>=Fg>$55wqZl4Cpnnz-pxYLEk(CT}78<}xhB|Gr z7Xc$g&INxM5$Pz<`I4;?C}dkYoZ7_p^R`NWyW;7V4kJdIfIe4lS)m)QuEkK>)o`gm z;p_)?o9Z(SzaX+1=M|(ACE)d=X#nfo&Td5)k$INy1XblVD;fIP09c8J{fN9c#*-^s zk>;znD^`xzp9KJ{Wa!4#+Z8KO_5k@#7kgA$ThkRf0K=NbBb9rnmC3;CB3g~e9Fz&b zud1A&q~ybzZ|i;jT^kh$D4iTB!J%qQTni7Rn4kDeWe$cB<2192LPbFa;E_ zq*-Q@?EZeoKeD0w_~o&i?JqNaXLe@y-19r@8$O2SWm{hZy zA-J8=PATkaTDwbF#A&nPDq+{g@Z&8#XQos7ii1zJ)SS6-@V5WB5r=C%oJ!kkael$# zHF||xak!V#wqrVH;NCXVHNqnLiOJejb*)=*xI#D<2wwnl_T(CJ_MXPOmbnp&D}+@F zO2IK8m8;`*C9Pdb>D)0Y$RKctT;cx*;ip?dUQ}0Q%WT!Bfc|WmqAlB2Z1ZuNaoPzF zrK%f2>au03^h+LLpmt%^iOWuRJg1$o6fQY$r4}tdGO}>-VcVf@;sGrqa}lNG(w75mRFu z>UI32q*l-xpcbVbLtW;3ElLeS17u{pRRLhB0eHfQ-ss&J-}0QIEwQG^jR@h)c$=U3 zfPzfy2u0A$Z=L8h5o{X-8_A3iuNO#-QBhgAlQdqFMOgch935|kNxf;Q!FWyyZ8Q@{ z9%MYeoZp3bQ-p7{#e=^rQ0cq;hwl?R{Q%09j*reBFSZW^7iSBifygo7{in%auchWI z*@cNWyR~EgEiVJ>`cu&qnA;ZLc&P?@-!JnfH#2j6gmp*uzmjJPGljkhqJNnz?Rp6Yhukk zk0|;8$|46f1MD?E&)5BaO^ytW)z}_i4aRB!YCU*rY-OGi>QTGSBleT|25hgd-=xQG z6=Yb)U*536P5{G#4g)8SCl@5%U2$^0N9?UY=OiH?Q!&?f#BXzWsi!cb1Ax7k-B*|P zCBLW2mvPRD6C%>x7Vp|V6MMxX+T61V)%$TCVv6$Ms6Tm2=e|&4I$K4ZctltA1h9Ds z)UmP=*lT;dH)iI7S!36#JJ0f1-up%9m_+4yG4jQ>16|L|)TU>8l~efjOZ_k3uq3c! zRDBlE=&;%8KTB*Bh0Xh&bI&q*b}wUtiU^{`WV( zP>k)8&$Obe?*1^gy#u&p5|y~It<9|mxBR^ryN@;h-~*O=9)w8`4364jU$-HC`;npo z&R|xDcu%*4yeHGuH=%kLQ0<^Hf)IHv*>L9#M{Y%p6kPh}%>s`)a`E609Itj!zaTb`S3tYtf<4G}UQZK}RWMeoaNdZK+G zxNaskqJf@1(DrPZDqm|K?Aj_wN%KKI%2pnR2-yPv$d{|~)0tb*LS3WB%0(8+To2nu zzmO+-V{9lH%h&5$b~oMj`dlAF$g9BGYr9|h=b3Dv{` zkzJ~U^L&a3!OB;T#df#+r~x1|6A$Fr##~pfILktKcT>+rz{NR28J~c5H}x#>r>YKS zALZmjVVy~h{qc7ni<}Qm6P+&WYYaD)Bi6DI7P0dumJx&fNHh>x;}QLnz=bm~bDqbe zhoXT<+n7K3C@`Gk!<1+8n)N!|dqK!w(NJ}qtV%|Zoe0mU>QBhM@drqY@GZ6VI3nAD zs=RhprN;jk)9=R#XK2>=YU&fsXcgFf`M> za9g}eeibSs%~EYQ;eEOBZR;cv!cE@>YK7VM*^Lu; zD&PGCyUO<0MjBST^_M|_&V8XoTfD1Ngl@prNhZ#zP*h4}3WeuYxuat+_;9h# z%89-}&pN^04sw1m1w~Ok?4LjHVp035xh4zHG0?Tw*!F5gE-L1^{{dC7D_?T>#w-8; N002ovPDHLkV1hCFdf)&6 literal 0 HcmV?d00001 diff --git a/src/user/mod.rs b/src/user/mod.rs index e1fafb5801e..d2ec9da9e69 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -387,6 +387,7 @@ pub struct FavoriteUser { target_id: i32, } + fn favorite_target(req: &mut Request) -> CargoResult { let user = req.user()?; let target_user_id: i32 = req.params()["user_id"].parse().expect("User ID not found"); From c854bd562ead170615508a73f81fe2d9c6f24f17 Mon Sep 17 00:00:00 2001 From: PritiKumr Date: Tue, 20 Jun 2017 18:50:09 +0530 Subject: [PATCH 04/11] using opacity instead of display:none for remove favorite user option in dashboard --- app/styles/me.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/styles/me.scss b/app/styles/me.scss index 47d8e109b9c..9fdfabd0a22 100644 --- a/app/styles/me.scss +++ b/app/styles/me.scss @@ -174,7 +174,7 @@ } .user-options { padding-left: 5px; - display: none; + opacity: 0; .unfavorite-user { width: 10px; height: 10px; @@ -182,7 +182,7 @@ } &:hover .user-options{ - display: block; + opacity: 1; } } } From 4a1c0b58dc09cde6025d03d2ea607a63b23ea3b1 Mon Sep 17 00:00:00 2001 From: Steve Robinson Date: Tue, 1 Aug 2017 17:45:35 +0530 Subject: [PATCH 05/11] using inner join approach to find users favorited by a user --- src/user/mod.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/user/mod.rs b/src/user/mod.rs index d2ec9da9e69..3268da50017 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -28,7 +28,7 @@ pub mod middleware; /// The model representing a row in the `users` database table. #[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, Associations)] -#[has_many(favorite_users)] +#[primary_key(id)] pub struct User { pub id: i32, pub email: Option, @@ -379,7 +379,6 @@ pub fn show_team(req: &mut Request) -> CargoResult { } #[derive(Insertable, Queryable, Identifiable, Associations)] -#[belongs_to(User)] #[primary_key(user_id, target_id)] #[table_name="favorite_users"] pub struct FavoriteUser { @@ -387,6 +386,7 @@ pub struct FavoriteUser { target_id: i32, } +joinable!(favorite_users -> users(target_id)); fn favorite_target(req: &mut Request) -> CargoResult { let user = req.user()?; @@ -434,20 +434,15 @@ pub fn favorited(req: &mut Request) -> CargoResult { Ok(req.json(&R { favorited: favorited })) } - - - - /// Handles the `GET /users/:user_id/favorite_users` route. pub fn favorite_users(req: &mut Request) -> CargoResult { let user_id: i32 = req.params()["user_id"].parse() .expect("User ID not found"); let conn = req.db_conn()?; - let users = users::table.filter(users::id.eq_any( - favorite_users::table.select(favorite_users::target_id) - .filter(favorite_users::user_id.eq(user_id)) - )).select(users::all_columns) + let users = users::table.inner_join(favorite_users::table) + .filter(favorite_users::user_id.eq(user_id)) + .select(users::all_columns) .load::(&*conn)? .into_iter().map(|u| u.encodable()).collect(); @@ -456,9 +451,6 @@ pub fn favorite_users(req: &mut Request) -> CargoResult { Ok(req.json(&R{ users: users })) } - - - /// Handles the `GET /me/updates` route. pub fn updates(req: &mut Request) -> CargoResult { use diesel::expression::dsl::any; From 3a262bd3ed5a983e7157d8b37a2a9fd4f350d420 Mon Sep 17 00:00:00 2001 From: PritiKumr Date: Tue, 1 Aug 2017 17:53:56 +0530 Subject: [PATCH 06/11] Clean up --- app/routes/dashboard.js | 5 ++++- app/routes/user.js | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/routes/dashboard.js b/app/routes/dashboard.js index 4ce4cfddab9..f1a13f6e92c 100644 --- a/app/routes/dashboard.js +++ b/app/routes/dashboard.js @@ -34,12 +34,15 @@ export default Route.extend(AuthenticatedRoute, { following: 1 }); + let myStats = user.stats(); + let favoriteUsers = user.favoriteUsers(); return RSVP.hash({ myCrates, myFollowing, - favoriteUsers + myStats, + favoriteUsers, }).then((hash) => { this.set('data', hash); }); diff --git a/app/routes/user.js b/app/routes/user.js index 1101982dbab..70c3dade101 100644 --- a/app/routes/user.js +++ b/app/routes/user.js @@ -1,4 +1,3 @@ -<<<<<<< 120f8008fb08c21fc6cd239c1100692a0ff487e6 import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import RSVP from 'rsvp'; @@ -44,7 +43,7 @@ export default Route.extend({ this.session.get('currentUser') !== model.user ); - if (controller.get('allowFavorting')) { + if (controller.get('allowFavoriting')) { ajax(`/api/v1/users/${model.user.id}/favorited`) .then((d) => controller.set('favorited', d.favorited)) .finally(() => controller.set('fetchingFavorite', false)); From f61821501f79f787e99ba98bcfa700dafd4fad1e Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 17 Jun 2019 14:17:16 -0600 Subject: [PATCH 07/11] Resolving more merge issues --- src/controllers/user/me.rs | 63 +++++ src/models/user.rs | 8 + src/schema.rs | 2 +- src/user/mod.rs | 516 ------------------------------------- 4 files changed, 72 insertions(+), 517 deletions(-) delete mode 100644 src/user/mod.rs diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index d8100eded9e..33aed84fa55 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -45,6 +45,69 @@ pub fn me(req: &mut dyn Request) -> CargoResult { })) } +fn favorite_target(req: &mut Request) -> CargoResult { + let user = req.user()?; + let target_user_id: i32 = req.params()["user_id"].parse().expect("User ID not found"); + Ok(FavoriteUser { + user_id: user.id, + target_id: target_user_id, + }) +} + +/// Handles the `PUT /users/:user_id/favorite` route. +pub fn favorite(req: &mut Request) -> CargoResult { + use diesel::pg::upsert::OnConflictExtension; + + let favorite = favorite_target(req)?; + let conn = req.db_conn()?; + diesel::insert(&favorite.on_conflict_do_nothing()) + .into(favorite_users::table) + .execute(&*conn)?; + #[derive(RustcEncodable)] + struct R { ok: bool } + Ok(req.json(&R { ok: true })) +} + +/// Handles the `DELETE /users/:user_id/favorite` route. +pub fn unfavorite(req: &mut Request) -> CargoResult { + let favorite = favorite_target(req)?; + let conn = req.db_conn()?; + diesel::delete(&favorite).execute(&*conn)?; + #[derive(RustcEncodable)] + struct R { ok: bool } + Ok(req.json(&R { ok: true })) +} + +/// Handles the `GET /users/:user_id/favorited` route. +pub fn favorited(req: &mut Request) -> CargoResult { + use diesel::expression::dsl::exists; + + let fav = favorite_target(req)?; + let conn = req.db_conn()?; + let favorited = diesel::select(exists(favorite_users::table.find(fav.id()))) + .get_result(&*conn)?; + #[derive(RustcEncodable)] + struct R { favorited: bool } + Ok(req.json(&R { favorited: favorited })) +} + +/// Handles the `GET /users/:user_id/favorite_users` route. +pub fn favorite_users(req: &mut Request) -> CargoResult { + let user_id: i32 = req.params()["user_id"].parse() + .expect("User ID not found"); + let conn = req.db_conn()?; + + let users = users::table.inner_join(favorite_users::table) + .filter(favorite_users::user_id.eq(user_id)) + .select(users::all_columns) + .load::(&*conn)? + .into_iter().map(|u| u.encodable()).collect(); + + #[derive(RustcEncodable)] + struct R { users: Vec } + Ok(req.json(&R{ users: users })) +} + /// Handles the `GET /me/updates` route. pub fn updates(req: &mut dyn Request) -> CargoResult { use diesel::dsl::any; diff --git a/src/models/user.rs b/src/models/user.rs index 2f22c20a19e..7eee30ab3d1 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -32,6 +32,14 @@ pub struct NewUser<'a> { pub gh_access_token: Cow<'a, str>, } +#[derive(Insertable, Queryable, Identifiable, Associations)] +#[primary_key(user_id, target_id)] +#[table_name="favorite_users"] +pub struct FavoriteUser { + user_id: i32, + target_id: i32, +} + impl<'a> NewUser<'a> { pub fn new( gh_id: i32, diff --git a/src/schema.rs b/src/schema.rs index 01d7576d584..a4a0dd7b151 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -973,7 +973,6 @@ table! { } } -// XXX TODO make `favorite_users` joinable with something? joinable!(api_tokens -> users (user_id)); joinable!(crate_owner_invitations -> crates (crate_id)); joinable!(crate_owners -> crates (crate_id)); @@ -986,6 +985,7 @@ joinable!(crates_keywords -> keywords (keyword_id)); joinable!(dependencies -> crates (crate_id)); joinable!(dependencies -> versions (version_id)); joinable!(emails -> users (user_id)); +joinable!(favorite_users -> users (target_id)); joinable!(follows -> crates (crate_id)); joinable!(follows -> users (user_id)); joinable!(publish_limit_buckets -> users (user_id)); diff --git a/src/user/mod.rs b/src/user/mod.rs deleted file mode 100644 index 3268da50017..00000000000 --- a/src/user/mod.rs +++ /dev/null @@ -1,516 +0,0 @@ -use conduit::{Request, Response}; -use conduit_cookie::RequestSession; -use conduit_router::RequestParams; -use diesel::prelude::*; -use diesel; - -use diesel::pg::PgConnection; -use pg::GenericConnection; -use pg::rows::Row; -use rand::{thread_rng, Rng}; -use std::borrow::Cow; - -use app::RequestApp; -use db::RequestTransaction; -use krate::Follow; -use pagination::Paginate; -use schema::*; -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, AuthenticationSource}; - -pub mod middleware; - -/// The model representing a row in the `users` database table. -#[derive(Clone, Debug, PartialEq, Eq, Queryable, Identifiable, Associations)] -#[primary_key(id)] -pub struct User { - pub id: i32, - pub email: Option, - pub gh_access_token: String, - pub gh_login: String, - pub name: Option, - pub gh_avatar: Option, - pub gh_id: i32, -} - -#[derive(Insertable, AsChangeset, Debug)] -#[table_name = "users"] -pub struct NewUser<'a> { - pub gh_id: i32, - pub gh_login: &'a str, - pub email: Option<&'a str>, - pub name: Option<&'a str>, - pub gh_avatar: Option<&'a str>, - pub gh_access_token: Cow<'a, str>, -} - -impl<'a> NewUser<'a> { - pub fn new( - gh_id: i32, - gh_login: &'a str, - email: Option<&'a str>, - name: Option<&'a str>, - gh_avatar: Option<&'a str>, - gh_access_token: &'a str, - ) -> Self { - NewUser { - gh_id: gh_id, - gh_login: gh_login, - email: email, - name: name, - gh_avatar: gh_avatar, - gh_access_token: Cow::Borrowed(gh_access_token), - } - } - - /// Inserts the user into the database, or updates an existing one. - pub fn create_or_update(&self, conn: &PgConnection) -> QueryResult { - use diesel::insert; - use diesel::expression::dsl::sql; - use diesel::types::Integer; - use diesel::pg::upsert::*; - - let conflict_target = sql::("(gh_id) WHERE gh_id > 0"); - insert(&self.on_conflict(conflict_target, do_update().set(self))) - .into(users::table) - .get_result(conn) - .map_err(Into::into) - } -} - -/// The serialization format for the `User` model. -#[derive(Deserialize, Serialize, Debug)] -pub struct EncodableUser { - pub id: i32, - pub login: String, - pub email: Option, - pub name: Option, - pub avatar: Option, - pub url: Option, -} - -impl User { - /// Queries the database for a user with a certain `gh_login` value. - pub fn find_by_login(conn: &GenericConnection, login: &str) -> CargoResult { - let stmt = conn.prepare( - "SELECT * FROM users - WHERE gh_login = $1", - )?; - let rows = stmt.query(&[&login])?; - let row = rows.iter().next().chain_error(|| NotFound)?; - Ok(Model::from_row(&row)) - } - - /// Queries the database for a user with a certain `api_token` value. - pub fn find_by_api_token(conn: &PgConnection, token_: &str) -> CargoResult { - use diesel::update; - use diesel::expression::now; - use schema::api_tokens::dsl::{api_tokens, token, user_id, last_used_at}; - use schema::users::dsl::{users, id}; - let user_id_ = update(api_tokens.filter(token.eq(token_))) - .set(last_used_at.eq(now.nullable())) - .returning(user_id) - .get_result::(conn)?; - Ok(users.filter(id.eq(user_id_)).get_result(conn)?) - } - - /// Updates a user or inserts a new user into the database. - pub fn find_or_insert( - conn: &GenericConnection, - id: i32, - login: &str, - email: Option<&str>, - name: Option<&str>, - avatar: Option<&str>, - access_token: &str, - ) -> CargoResult { - // TODO: this is racy, but it looks like any other solution is... - // interesting! For now just do the racy thing which will report - // more errors than it needs to. - - let stmt = conn.prepare( - "UPDATE users - SET gh_access_token = $1, - email = $2, - name = $3, - gh_avatar = $4, - gh_login = $5 - WHERE gh_id = $6 - RETURNING *", - )?; - let rows = stmt.query( - &[&access_token, &email, &name, &avatar, &login, &id], - )?; - if let Some(ref row) = rows.iter().next() { - return Ok(Model::from_row(row)); - } - let stmt = conn.prepare( - "INSERT INTO users - (email, gh_access_token, - gh_login, name, gh_avatar, gh_id) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING *", - )?; - let rows = stmt.query( - &[&email, &access_token, &login, &name, &avatar, &id], - )?; - Ok(Model::from_row(&rows.iter().next().chain_error(|| { - internal("no user with email we just found") - })?)) - } - - 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 { - id, - email, - name, - gh_login, - gh_avatar, - .. - } = self; - let url = format!("https://github.com/{}", gh_login); - EncodableUser { - id: id, - email: email, - avatar: gh_avatar, - login: gh_login, - name: name, - url: Some(url), - } - } -} - -impl Model for User { - fn from_row(row: &Row) -> User { - User { - id: row.get("id"), - email: row.get("email"), - gh_access_token: row.get("gh_access_token"), - gh_login: row.get("gh_login"), - gh_id: row.get("gh_id"), - name: row.get("name"), - gh_avatar: row.get("gh_avatar"), - } - } - - fn table_name(_: Option) -> &'static str { - "users" - } -} - -/// Handles the `GET /authorize_url` route. -/// -/// This route will return an authorization URL for the GitHub OAuth flow including the crates.io -/// `client_id` and a randomly generated `state` secret. -/// -/// see https://developer.github.com/v3/oauth/#redirect-users-to-request-github-access -/// -/// ## Response Body Example -/// -/// ```json -/// { -/// "state": "b84a63c4ea3fcb4ac84", -/// "url": "https://github.com/login/oauth/authorize?client_id=...&state=...&scope=read%3Aorg" -/// } -/// ``` -pub fn github_authorize(req: &mut Request) -> CargoResult { - // Generate a random 16 char ASCII string - let state: String = thread_rng().gen_ascii_chars().take(16).collect(); - req.session().insert( - "github_oauth_state".to_string(), - state.clone(), - ); - - let url = req.app().github.authorize_url(state.clone()); - - #[derive(Serialize)] - struct R { - url: String, - state: String, - } - Ok(req.json(&R { - url: url.to_string(), - state: state, - })) -} - -/// Handles the `GET /authorize` route. -/// -/// This route is called from the GitHub API OAuth flow after the user accepted or rejected -/// the data access permissions. It will check the `state` parameter and then call the GitHub API -/// to exchange the temporary `code` for an API token. The API token is returned together with -/// the corresponding user information. -/// -/// see https://developer.github.com/v3/oauth/#github-redirects-back-to-your-site -/// -/// ## Query Parameters -/// -/// - `code` – temporary code received from the GitHub API **(Required)** -/// - `state` – state parameter received from the GitHub API **(Required)** -/// -/// ## Response Body Example -/// -/// ```json -/// { -/// "api_token": "b84a63c4ea3fcb4ac84", -/// "user": { -/// "email": "foo@bar.org", -/// "name": "Foo Bar", -/// "login": "foobar", -/// "avatar": "https://avatars.githubusercontent.com/u/1234", -/// "url": null -/// } -/// } -/// ``` -pub fn github_access_token(req: &mut Request) -> CargoResult { - // Parse the url query - let mut query = req.query(); - let code = query.remove("code").unwrap_or_default(); - let state = query.remove("state").unwrap_or_default(); - - // Make sure that the state we just got matches the session state that we - // should have issued earlier. - { - let session_state = req.session().remove(&"github_oauth_state".to_string()); - let session_state = session_state.as_ref().map(|a| &a[..]); - if Some(&state[..]) != session_state { - return Err(human("invalid state parameter")); - } - } - - #[derive(Deserialize)] - struct GithubUser { - email: Option, - name: Option, - login: String, - id: i32, - avatar_url: Option, - } - - // Fetch the access token from github using the code we just got - let token = req.app().github.exchange(code.clone()).map_err( - |s| human(&s), - )?; - - let (handle, resp) = http::github(req.app(), "/user", &token)?; - let ghuser: GithubUser = http::parse_github_response(handle, &resp)?; - - let user = NewUser::new( - ghuser.id, - &ghuser.login, - ghuser.email.as_ref().map(|s| &s[..]), - ghuser.name.as_ref().map(|s| &s[..]), - ghuser.avatar_url.as_ref().map(|s| &s[..]), - &token.access_token, - ).create_or_update(&*req.db_conn()?)?; - req.session().insert( - "user_id".to_string(), - user.id.to_string(), - ); - req.mut_extensions().insert(user); - me(req) -} - -/// Handles the `GET /logout` route. -pub fn logout(req: &mut Request) -> CargoResult { - req.session().remove(&"user_id".to_string()); - Ok(req.json(&true)) -} - -/// Handles the `GET /me` route. -pub fn me(req: &mut Request) -> CargoResult { - #[derive(Serialize)] - struct R { - user: EncodableUser, - } - Ok(req.json(&R { user: req.user()?.clone().encodable() })) -} - -/// Handles the `GET /users/:user_id` route. -pub fn show(req: &mut Request) -> CargoResult { - use self::users::dsl::{users, gh_login}; - - let name = &req.params()["user_id"]; - let conn = req.db_conn()?; - let user = users.filter(gh_login.eq(name)).first::(&*conn)?; - - #[derive(Serialize)] - struct R { - user: EncodableUser, - } - 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(Serialize)] - struct R { - team: EncodableTeam, - } - Ok(req.json(&R { team: team.encodable() })) -} - -#[derive(Insertable, Queryable, Identifiable, Associations)] -#[primary_key(user_id, target_id)] -#[table_name="favorite_users"] -pub struct FavoriteUser { - user_id: i32, - target_id: i32, -} - -joinable!(favorite_users -> users(target_id)); - -fn favorite_target(req: &mut Request) -> CargoResult { - let user = req.user()?; - let target_user_id: i32 = req.params()["user_id"].parse().expect("User ID not found"); - Ok(FavoriteUser { - user_id: user.id, - target_id: target_user_id, - }) -} - -/// Handles the `PUT /users/:user_id/favorite` route. -pub fn favorite(req: &mut Request) -> CargoResult { - use diesel::pg::upsert::OnConflictExtension; - - let favorite = favorite_target(req)?; - let conn = req.db_conn()?; - diesel::insert(&favorite.on_conflict_do_nothing()) - .into(favorite_users::table) - .execute(&*conn)?; - #[derive(RustcEncodable)] - struct R { ok: bool } - Ok(req.json(&R { ok: true })) -} - -/// Handles the `DELETE /users/:user_id/favorite` route. -pub fn unfavorite(req: &mut Request) -> CargoResult { - let favorite = favorite_target(req)?; - let conn = req.db_conn()?; - diesel::delete(&favorite).execute(&*conn)?; - #[derive(RustcEncodable)] - struct R { ok: bool } - Ok(req.json(&R { ok: true })) -} - -/// Handles the `GET /users/:user_id/favorited` route. -pub fn favorited(req: &mut Request) -> CargoResult { - use diesel::expression::dsl::exists; - - let fav = favorite_target(req)?; - let conn = req.db_conn()?; - let favorited = diesel::select(exists(favorite_users::table.find(fav.id()))) - .get_result(&*conn)?; - #[derive(RustcEncodable)] - struct R { favorited: bool } - Ok(req.json(&R { favorited: favorited })) -} - -/// Handles the `GET /users/:user_id/favorite_users` route. -pub fn favorite_users(req: &mut Request) -> CargoResult { - let user_id: i32 = req.params()["user_id"].parse() - .expect("User ID not found"); - let conn = req.db_conn()?; - - let users = users::table.inner_join(favorite_users::table) - .filter(favorite_users::user_id.eq(user_id)) - .select(users::all_columns) - .load::(&*conn)? - .into_iter().map(|u| u.encodable()).collect(); - - #[derive(RustcEncodable)] - struct R { users: Vec } - Ok(req.json(&R{ users: users })) -} - -/// Handles the `GET /me/updates` route. -pub fn updates(req: &mut Request) -> CargoResult { - use diesel::expression::dsl::any; - - let user = req.user()?; - let (offset, limit) = req.pagination(10, 100)?; - let conn = req.db_conn()?; - - let followed_crates = Follow::belonging_to(user).select(follows::crate_id); - let data = versions::table - .inner_join(crates::table) - .filter(crates::id.eq(any(followed_crates))) - .order(versions::created_at.desc()) - .select((versions::all_columns, crates::name)) - .paginate(limit, offset) - .load::<((Version, String), i64)>(&*conn)?; - - let more = data.get(0) - .map(|&(_, count)| count > offset + limit) - .unwrap_or(false); - - let versions = data.into_iter() - .map(|((version, crate_name), _)| version.encodable(&crate_name)) - .collect(); - - #[derive(Serialize)] - struct R { - versions: Vec, - meta: Meta, - } - #[derive(Serialize)] - struct Meta { - more: bool, - } - Ok(req.json(&R { - versions: versions, - meta: Meta { more: more }, - })) -} - -/// Handles the `GET /users/:user_id/stats` route. -pub fn stats(req: &mut Request) -> CargoResult { - use diesel::expression::dsl::sum; - use owner::OwnerKind; - - let user_id = &req.params()["user_id"].parse::().ok().unwrap(); - let conn = req.db_conn()?; - - let data = crate_owners::table - .inner_join(crates::table) - .filter(crate_owners::owner_id.eq(user_id).and( - crate_owners::owner_kind.eq(OwnerKind::User as i32), - )) - .select(sum(crates::downloads)) - .first::>(&*conn)? - .unwrap_or(0); - - #[derive(Serialize)] - struct R { - total_downloads: i64, - } - Ok(req.json(&R { total_downloads: data })) -} From 3cf43d775c7985e73a0d74b6a2a5d0efff83dff2 Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 17 Jun 2019 15:53:31 -0600 Subject: [PATCH 08/11] Successful build :D --- src/controllers/user/me.rs | 35 +++++++++++++++++------------------ src/models.rs | 2 +- src/models/user.rs | 8 ++++---- src/router.rs | 8 ++++---- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index 33aed84fa55..308f33d1bae 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -5,9 +5,9 @@ use crate::email; use crate::util::bad_request; use crate::util::errors::CargoError; -use crate::models::{Email, Follow, NewEmail, User, Version}; -use crate::schema::{crates, emails, follows, users, versions}; -use crate::views::{EncodableMe, EncodableVersion}; +use crate::models::{Email, FavoriteUser, Follow, NewEmail, User, Version}; +use crate::schema::{crates, emails, favorite_users, follows, users, versions}; +use crate::views::{EncodableMe, EncodablePublicUser, EncodableVersion}; /// Handles the `GET /me` route. pub fn me(req: &mut dyn Request) -> CargoResult { @@ -45,7 +45,7 @@ pub fn me(req: &mut dyn Request) -> CargoResult { })) } -fn favorite_target(req: &mut Request) -> CargoResult { +fn favorite_target(req: &mut dyn Request) -> CargoResult { let user = req.user()?; let target_user_id: i32 = req.params()["user_id"].parse().expect("User ID not found"); Ok(FavoriteUser { @@ -55,44 +55,43 @@ fn favorite_target(req: &mut Request) -> CargoResult { } /// Handles the `PUT /users/:user_id/favorite` route. -pub fn favorite(req: &mut Request) -> CargoResult { - use diesel::pg::upsert::OnConflictExtension; - +pub fn favorite(req: &mut dyn Request) -> CargoResult { let favorite = favorite_target(req)?; let conn = req.db_conn()?; - diesel::insert(&favorite.on_conflict_do_nothing()) - .into(favorite_users::table) + diesel::insert_into(favorite_users::table) + .values(&favorite) + .on_conflict_do_nothing() .execute(&*conn)?; - #[derive(RustcEncodable)] + #[derive(Serialize)] struct R { ok: bool } Ok(req.json(&R { ok: true })) } /// Handles the `DELETE /users/:user_id/favorite` route. -pub fn unfavorite(req: &mut Request) -> CargoResult { +pub fn unfavorite(req: &mut dyn Request) -> CargoResult { let favorite = favorite_target(req)?; let conn = req.db_conn()?; diesel::delete(&favorite).execute(&*conn)?; - #[derive(RustcEncodable)] + #[derive(Serialize)] struct R { ok: bool } Ok(req.json(&R { ok: true })) } /// Handles the `GET /users/:user_id/favorited` route. -pub fn favorited(req: &mut Request) -> CargoResult { +pub fn favorited(req: &mut dyn Request) -> CargoResult { use diesel::expression::dsl::exists; let fav = favorite_target(req)?; let conn = req.db_conn()?; let favorited = diesel::select(exists(favorite_users::table.find(fav.id()))) .get_result(&*conn)?; - #[derive(RustcEncodable)] + #[derive(Serialize)] struct R { favorited: bool } Ok(req.json(&R { favorited: favorited })) } /// Handles the `GET /users/:user_id/favorite_users` route. -pub fn favorite_users(req: &mut Request) -> CargoResult { +pub fn favorite_users(req: &mut dyn Request) -> CargoResult { let user_id: i32 = req.params()["user_id"].parse() .expect("User ID not found"); let conn = req.db_conn()?; @@ -101,10 +100,10 @@ pub fn favorite_users(req: &mut Request) -> CargoResult { .filter(favorite_users::user_id.eq(user_id)) .select(users::all_columns) .load::(&*conn)? - .into_iter().map(|u| u.encodable()).collect(); + .into_iter().map(|u| u.encodable_public()).collect(); - #[derive(RustcEncodable)] - struct R { users: Vec } + #[derive(Serialize)] + struct R { users: Vec } Ok(req.json(&R{ users: users })) } diff --git a/src/models.rs b/src/models.rs index 9968177d294..ca7c41e2054 100644 --- a/src/models.rs +++ b/src/models.rs @@ -11,7 +11,7 @@ pub use self::owner::{CrateOwner, Owner, OwnerKind}; pub use self::rights::Rights; pub use self::team::{NewTeam, Team}; pub use self::token::ApiToken; -pub use self::user::{NewUser, User}; +pub use self::user::{FavoriteUser, NewUser, User}; pub use self::version::{NewVersion, Version}; pub mod helpers; diff --git a/src/models/user.rs b/src/models/user.rs index 7eee30ab3d1..00456654177 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -6,7 +6,7 @@ use crate::app::App; use crate::util::CargoResult; use crate::models::{Crate, CrateOwner, Email, NewEmail, Owner, OwnerKind, Rights}; -use crate::schema::{crate_owners, emails, users}; +use crate::schema::{crate_owners, emails, favorite_users, users}; use crate::views::{EncodablePrivateUser, EncodablePublicUser}; /// The model representing a row in the `users` database table. @@ -32,12 +32,12 @@ pub struct NewUser<'a> { pub gh_access_token: Cow<'a, str>, } -#[derive(Insertable, Queryable, Identifiable, Associations)] +#[derive(Copy, Clone, Debug, Insertable, Queryable, Identifiable, Associations)] #[primary_key(user_id, target_id)] #[table_name="favorite_users"] pub struct FavoriteUser { - user_id: i32, - target_id: i32, + pub user_id: i32, + pub target_id: i32, } impl<'a> NewUser<'a> { diff --git a/src/router.rs b/src/router.rs index 48954741171..874e8d6d19d 100644 --- a/src/router.rs +++ b/src/router.rs @@ -76,10 +76,10 @@ pub fn build_router(app: &App) -> R404 { api_router.put("/users/:user_id", C(user::me::update_user)); api_router.get("/users/:user_id/stats", C(user::other::stats)); api_router.get("/teams/:team_id", C(team::show_team)); - api_router.get("/users/:user_id/favorited", C(user::favorited)); - api_router.put("/users/:user_id/favorite", C(user::favorite)); - api_router.delete("/users/:user_id/favorite", C(user::unfavorite)); - api_router.get("/users/:user_id/favorite_users", C(user::favorite_users)); + api_router.get("/users/:user_id/favorited", C(user::me::favorited)); + api_router.put("/users/:user_id/favorite", C(user::me::favorite)); + api_router.delete("/users/:user_id/favorite", C(user::me::unfavorite)); + api_router.get("/users/:user_id/favorite_users", C(user::me::favorite_users)); api_router.get("/me", C(user::me::me)); api_router.get("/me/updates", C(user::me::updates)); api_router.get("/me/tokens", C(token::list)); From 75ef9756d860017d6ac446dab2c41a169212b6ed Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 17 Jun 2019 18:37:18 -0600 Subject: [PATCH 09/11] Clippy --- src/controllers/user/me.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index 308f33d1bae..8f5637eed4f 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -87,7 +87,7 @@ pub fn favorited(req: &mut dyn Request) -> CargoResult { .get_result(&*conn)?; #[derive(Serialize)] struct R { favorited: bool } - Ok(req.json(&R { favorited: favorited })) + Ok(req.json(&R { favorited })) } /// Handles the `GET /users/:user_id/favorite_users` route. @@ -100,11 +100,11 @@ pub fn favorite_users(req: &mut dyn Request) -> CargoResult { .filter(favorite_users::user_id.eq(user_id)) .select(users::all_columns) .load::(&*conn)? - .into_iter().map(|u| u.encodable_public()).collect(); + .into_iter().map(User::encodable_public).collect(); #[derive(Serialize)] struct R { users: Vec } - Ok(req.json(&R{ users: users })) + Ok(req.json(&R{ users })) } /// Handles the `GET /me/updates` route. From 9bc76d20991020dfd7a79c52d8d93a7db464ccca Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 17 Jun 2019 18:38:33 -0600 Subject: [PATCH 10/11] Cargo fmt --- src/controllers/user/me.rs | 32 +++++++++++++++++++++----------- src/models/user.rs | 2 +- src/router.rs | 5 ++++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index 8f5637eed4f..f8473f3136a 100644 --- a/src/controllers/user/me.rs +++ b/src/controllers/user/me.rs @@ -63,7 +63,9 @@ pub fn favorite(req: &mut dyn Request) -> CargoResult { .on_conflict_do_nothing() .execute(&*conn)?; #[derive(Serialize)] - struct R { ok: bool } + struct R { + ok: bool, + } Ok(req.json(&R { ok: true })) } @@ -73,7 +75,9 @@ pub fn unfavorite(req: &mut dyn Request) -> CargoResult { let conn = req.db_conn()?; diesel::delete(&favorite).execute(&*conn)?; #[derive(Serialize)] - struct R { ok: bool } + struct R { + ok: bool, + } Ok(req.json(&R { ok: true })) } @@ -83,28 +87,34 @@ pub fn favorited(req: &mut dyn Request) -> CargoResult { let fav = favorite_target(req)?; let conn = req.db_conn()?; - let favorited = diesel::select(exists(favorite_users::table.find(fav.id()))) - .get_result(&*conn)?; + let favorited = + diesel::select(exists(favorite_users::table.find(fav.id()))).get_result(&*conn)?; #[derive(Serialize)] - struct R { favorited: bool } + struct R { + favorited: bool, + } Ok(req.json(&R { favorited })) } /// Handles the `GET /users/:user_id/favorite_users` route. pub fn favorite_users(req: &mut dyn Request) -> CargoResult { - let user_id: i32 = req.params()["user_id"].parse() - .expect("User ID not found"); + let user_id: i32 = req.params()["user_id"].parse().expect("User ID not found"); let conn = req.db_conn()?; - let users = users::table.inner_join(favorite_users::table) + let users = users::table + .inner_join(favorite_users::table) .filter(favorite_users::user_id.eq(user_id)) .select(users::all_columns) .load::(&*conn)? - .into_iter().map(User::encodable_public).collect(); + .into_iter() + .map(User::encodable_public) + .collect(); #[derive(Serialize)] - struct R { users: Vec } - Ok(req.json(&R{ users })) + struct R { + users: Vec, + } + Ok(req.json(&R { users })) } /// Handles the `GET /me/updates` route. diff --git a/src/models/user.rs b/src/models/user.rs index 00456654177..7ce2fe12b7d 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -34,7 +34,7 @@ pub struct NewUser<'a> { #[derive(Copy, Clone, Debug, Insertable, Queryable, Identifiable, Associations)] #[primary_key(user_id, target_id)] -#[table_name="favorite_users"] +#[table_name = "favorite_users"] pub struct FavoriteUser { pub user_id: i32, pub target_id: i32, diff --git a/src/router.rs b/src/router.rs index 874e8d6d19d..003bfab9525 100644 --- a/src/router.rs +++ b/src/router.rs @@ -79,7 +79,10 @@ pub fn build_router(app: &App) -> R404 { api_router.get("/users/:user_id/favorited", C(user::me::favorited)); api_router.put("/users/:user_id/favorite", C(user::me::favorite)); api_router.delete("/users/:user_id/favorite", C(user::me::unfavorite)); - api_router.get("/users/:user_id/favorite_users", C(user::me::favorite_users)); + api_router.get( + "/users/:user_id/favorite_users", + C(user::me::favorite_users), + ); api_router.get("/me", C(user::me::me)); api_router.get("/me/updates", C(user::me::updates)); api_router.get("/me/tokens", C(token::list)); From 30cae10d44c3bbd290701f325f7acbf1a086ff3f Mon Sep 17 00:00:00 2001 From: Kyle Strand Date: Mon, 17 Jun 2019 18:57:59 -0600 Subject: [PATCH 11/11] Eslint --- app/controllers/dashboard.js | 13 +-- app/controllers/user.js | 4 +- app/models/user.js | 1 - app/routes/dashboard.js | 15 ++-- app/routes/user.js | 9 +- package-lock.json | 162 ++++------------------------------- 6 files changed, 36 insertions(+), 168 deletions(-) diff --git a/app/controllers/dashboard.js b/app/controllers/dashboard.js index f8e8d947ccf..851f86a21f8 100644 --- a/app/controllers/dashboard.js +++ b/app/controllers/dashboard.js @@ -63,9 +63,12 @@ export default Controller.extend({ }, unfavoriteUser: function(user) { - this.store.adapterFor('user').unfavorite(user.id).then(() => { - this.get('favoriteUsers').users.removeObject(user); - }); - } - } + this.store + .adapterFor('user') + .unfavorite(user.id) + .then(() => { + this.get('favoriteUsers').users.removeObject(user); + }); + }, + }, }); diff --git a/app/controllers/user.js b/app/controllers/user.js index 982e40a36df..9613afe40b3 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -33,11 +33,9 @@ export default Controller.extend(PaginationMixin, { this.set('fetchingFavorite', true); let owner = this.get('user'); - let op = this.toggleProperty('favorited') ? - owner.favorite() : owner.unfavorite(); + let op = this.toggleProperty('favorited') ? owner.favorite() : owner.unfavorite(); return op.finally(() => this.set('fetchingFavorite', false)); }, }, - }); diff --git a/app/models/user.js b/app/models/user.js index 2511af659bb..3f0268743da 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -25,5 +25,4 @@ export default DS.Model.extend({ favoriteUsers() { return this.store.adapterFor('user').favoriteUsers(this.get('id')); }, - }); diff --git a/app/routes/dashboard.js b/app/routes/dashboard.js index 68bdc5f57ca..da6c6c71259 100644 --- a/app/routes/dashboard.js +++ b/app/routes/dashboard.js @@ -36,11 +36,14 @@ export default Route.extend(AuthenticatedRoute, { let favoriteUsers = user.favoriteUsers(); - this.set('data', await RSVP.hash({ - myCrates, - myFollowing, - myStats, - favoriteUsers - })); + this.set( + 'data', + await RSVP.hash({ + myCrates, + myFollowing, + myStats, + favoriteUsers, + }), + ); }, }); diff --git a/app/routes/user.js b/app/routes/user.js index f025b7fbde5..c67fb486c7b 100644 --- a/app/routes/user.js +++ b/app/routes/user.js @@ -36,14 +36,11 @@ export default Route.extend({ controller.set('fetchingFeed', true); controller.set('crates', this.get('data.crates')); controller.set('user', model.user); - controller.set( - 'allowFavorting', - this.session.get('currentUser') !== model.user - ); - + controller.set('allowFavorting', this.session.get('currentUser') !== model.user); + if (controller.get('allowFavoriting')) { ajax(`/api/v1/users/${model.user.id}/favorited`) - .then((d) => controller.set('favorited', d.favorited)) + .then(d => controller.set('favorited', d.favorited)) .finally(() => controller.set('fetchingFavorite', false)); } }, diff --git a/package-lock.json b/package-lock.json index f8f4169b4ab..04c4b2fc007 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10008,12 +10008,6 @@ "uri-js": "^4.2.2" } }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -10040,15 +10034,6 @@ "supports-color": "^5.3.0" } }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -10077,36 +10062,16 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, - "external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -10118,53 +10083,9 @@ } }, "globals": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz", - "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==", - "dev": true - }, - "inquirer": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz", - "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.11", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "js-yaml": { @@ -10190,61 +10111,17 @@ "dev": true }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "rxjs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", - "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", "dev": true }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -10262,15 +10139,6 @@ "requires": { "has-flag": "^3.0.0" } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } } } }, @@ -11546,9 +11414,9 @@ }, "dependencies": { "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -21523,9 +21391,9 @@ "dev": true }, "table": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz", - "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.1.tgz", + "integrity": "sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w==", "dev": true, "requires": { "ajv": "^6.9.1",