diff --git a/app/adapters/user.js b/app/adapters/user.js index ba343fc4cfd..96ea3664979 100644 --- a/app/adapters/user.js +++ b/app/adapters/user.js @@ -13,4 +13,20 @@ 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`; + }, + + 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 12bc169d9a7..851f86a21f8 100644 --- a/app/controllers/dashboard.js +++ b/app/controllers/dashboard.js @@ -15,6 +15,7 @@ export default Controller.extend({ this.myFollowing = A(); this.myFeed = A(); this.myStats = 0; + this.favoriteUsers = []; }, visibleCrates: computed('myCrates.[]', function() { @@ -33,10 +34,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: { async loadMore() { this.set('loadingMore', true); @@ -52,5 +61,14 @@ export default Controller.extend({ 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/controllers/user.js b/app/controllers/user.js index 08b51995c2d..9613afe40b3 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -24,4 +24,18 @@ 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 9bab18b285d..3f0268743da 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -13,4 +13,16 @@ export default DS.Model.extend({ stats() { return this.store.adapterFor('user').stats(this.id); }, + + favorite() { + return this.store.adapterFor('user').favorite(this.get('id')); + }, + + unfavorite() { + return this.store.adapterFor('user').unfavorite(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 8e13703b0c7..da6c6c71259 100644 --- a/app/routes/dashboard.js +++ b/app/routes/dashboard.js @@ -11,6 +11,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.loadingMore) { controller.set('myFeed', A()); @@ -33,6 +34,16 @@ export default Route.extend(AuthenticatedRoute, { let myStats = user.stats(); - this.set('data', await RSVP.hash({ myCrates, myFollowing, myStats })); + let favoriteUsers = user.favoriteUsers(); + + this.set( + 'data', + await RSVP.hash({ + myCrates, + myFollowing, + myStats, + favoriteUsers, + }), + ); }, }); diff --git a/app/routes/user.js b/app/routes/user.js index 265e31cc7f8..c67fb486c7b 100644 --- a/app/routes/user.js +++ b/app/routes/user.js @@ -1,6 +1,7 @@ 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(), @@ -28,4 +29,19 @@ export default Route.extend({ }, ); }, + + setupController(controller, model) { + this._super(controller, model); + + 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('allowFavoriting')) { + ajax(`/api/v1/users/${model.user.id}/favorited`) + .then(d => controller.set('favorited', d.favorited)) + .finally(() => controller.set('fetchingFavorite', false)); + } + }, }); diff --git a/app/styles/app.scss b/app/styles/app.scss index a612f8a7f33..7eed3271890 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -345,6 +345,88 @@ h1 { } } +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; + } +} + .arrow-in-list svg { background: #fff; } diff --git a/app/styles/me.scss b/app/styles/me.scss index 0f09ce629a6..7f6c23113ff 100644 --- a/app/styles/me.scss +++ b/app/styles/me.scss @@ -145,7 +145,7 @@ @include order(1); margin-right: 0; } - #my-crates, #my-following { margin: 0; } + #my-crates, #my-following, #my-favorite-users { margin: 0; } } } @@ -283,3 +283,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; + opacity: 0; + .unfavorite-user { + width: 10px; + height: 10px; + } + } + + &:hover .user-options{ + opacity: 1; + } + } + } +} 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 @@ + diff --git a/app/templates/dashboard.hbs b/app/templates/dashboard.hbs index 7a414332d7f..17a8bdfb3b1 100644 --- a/app/templates/dashboard.hbs +++ b/app/templates/dashboard.hbs @@ -45,6 +45,15 @@ {{crate-downloads-list crates=visibleFollowing}} +
+
+

+ + Favorite Users +

+
+ {{favorite-users users=favoriteUsers.users unfavoriteUser=(action "unfavoriteUser")}} +
diff --git a/app/templates/user.hbs b/app/templates/user.hbs index 69ce4ce34da..4296beb19ec 100644 --- a/app/templates/user.hbs +++ b/app/templates/user.hbs @@ -1,11 +1,30 @@ -
- {{user-avatar user=model.user size='medium' data-test-avatar=true}} -

- {{ model.user.login }} -

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

+ {{ 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/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", diff --git a/public/assets/delete.png b/public/assets/delete.png new file mode 100644 index 00000000000..dae3af91637 Binary files /dev/null and b/public/assets/delete.png differ diff --git a/public/assets/favorite.png b/public/assets/favorite.png new file mode 100644 index 00000000000..85a0fe542af Binary files /dev/null and b/public/assets/favorite.png differ diff --git a/src/controllers/user/me.rs b/src/controllers/user/me.rs index d8100eded9e..f8473f3136a 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,6 +45,78 @@ pub fn me(req: &mut dyn 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 { + user_id: user.id, + target_id: target_user_id, + }) +} + +/// Handles the `PUT /users/:user_id/favorite` route. +pub fn favorite(req: &mut dyn Request) -> CargoResult { + let favorite = favorite_target(req)?; + let conn = req.db_conn()?; + diesel::insert_into(favorite_users::table) + .values(&favorite) + .on_conflict_do_nothing() + .execute(&*conn)?; + #[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 dyn Request) -> CargoResult { + let favorite = favorite_target(req)?; + let conn = req.db_conn()?; + diesel::delete(&favorite).execute(&*conn)?; + #[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 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(Serialize)] + 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 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(User::encodable_public) + .collect(); + + #[derive(Serialize)] + struct R { + users: Vec, + } + Ok(req.json(&R { users })) +} + /// Handles the `GET /me/updates` route. pub fn updates(req: &mut dyn Request) -> CargoResult { use diesel::dsl::any; 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 2f22c20a19e..7ce2fe12b7d 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,6 +32,14 @@ pub struct NewUser<'a> { pub gh_access_token: Cow<'a, str>, } +#[derive(Copy, Clone, Debug, Insertable, Queryable, Identifiable, Associations)] +#[primary_key(user_id, target_id)] +#[table_name = "favorite_users"] +pub struct FavoriteUser { + pub user_id: i32, + pub target_id: i32, +} + impl<'a> NewUser<'a> { pub fn new( gh_id: i32, diff --git a/src/router.rs b/src/router.rs index efea8b8f680..003bfab9525 100644 --- a/src/router.rs +++ b/src/router.rs @@ -76,6 +76,13 @@ 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::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)); diff --git a/src/schema.rs b/src/schema.rs index 6fd4bfb22d7..a4a0dd7b151 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -943,6 +943,13 @@ table! { } } +table! { + favorite_users (user_id, target_id) { + user_id -> Int4, + target_id -> Int4, + } +} + table! { use diesel::sql_types::*; use diesel_full_text_search::{TsVector as Tsvector}; @@ -978,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)); @@ -1003,6 +1011,7 @@ allow_tables_to_appear_in_same_query!( crates_keywords, dependencies, emails, + favorite_users, follows, keywords, metadata,