From d69c5eebcf72d41ff2272c23caca17860333cc5b Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Mon, 13 Jan 2020 11:20:26 -0300 Subject: [PATCH] Feature: Mass award badge (#8694) * UI: Mass grant a badge from the admin ui * Send the uploaded CSV and badge ID to the backend * Read the CSV and grant badge in batches * UX: Communicate the result to the user * Don't award if badge is disabled * Create a 'send_notification' method to remove duplicated code, slightly shrink badge image. Replace router transition with href. * Dynamically discover current route --- .../controllers/admin-badges-award.js.es6 | 35 +++++++++ .../admin/controllers/admin-badges.js.es6 | 18 ++++- .../admin/routes/admin-badges-award.js.es6 | 12 +++ .../admin/routes/admin-route-map.js.es6 | 1 + .../admin/templates/badges-award.hbs | 22 ++++++ .../javascripts/admin/templates/badges.hbs | 7 +- .../stylesheets/common/admin/badges.scss | 30 ++++++++ app/controllers/admin/badges_controller.rb | 34 +++++++++ app/jobs/regular/mass_award_badge.rb | 14 ++++ app/services/badge_granter.rb | 74 +++++++++++-------- config/locales/client.en.yml | 7 ++ config/routes.rb | 2 + spec/fixtures/csv/user_emails.csv | 4 + spec/jobs/mass_award_badge_spec.rb | 32 ++++++++ spec/requests/admin/badges_controller_spec.rb | 33 +++++++++ 15 files changed, 291 insertions(+), 34 deletions(-) create mode 100644 app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-badges-award.js.es6 create mode 100644 app/assets/javascripts/admin/templates/badges-award.hbs create mode 100644 app/jobs/regular/mass_award_badge.rb create mode 100644 spec/fixtures/csv/user_emails.csv create mode 100644 spec/jobs/mass_award_badge_spec.rb diff --git a/app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 new file mode 100644 index 0000000000..af3afd528b --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-badges-award.js.es6 @@ -0,0 +1,35 @@ +import Controller from "@ember/controller"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +export default Controller.extend({ + saving: false, + + actions: { + massAward() { + const file = document.querySelector("#massAwardCSVUpload").files[0]; + + if (this.model && file) { + const options = { + type: "POST", + processData: false, + contentType: false, + data: new FormData() + }; + + options.data.append("file", file); + + this.set("saving", true); + + ajax(`/admin/badges/award/${this.model.id}`, options) + .then(() => { + bootbox.alert(I18n.t("admin.badges.mass_award.success")); + }) + .catch(popupAjaxError) + .finally(() => this.set("saving", false)); + } else { + bootbox.alert(I18n.t("admin.badges.mass_award.aborted")); + } + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges.js.es6 index cf6c4e3aa2..4797fb2695 100644 --- a/app/assets/javascripts/admin/controllers/admin-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-badges.js.es6 @@ -1,2 +1,18 @@ import Controller from "@ember/controller"; -export default Controller.extend(); +import { inject as service } from "@ember/service"; +import discourseComputed from "discourse-common/utils/decorators"; + +export default Controller.extend({ + routing: service("-routing"), + + @discourseComputed("routing.currentRouteName") + selectedRoute() { + const currentRoute = this.routing.currentRouteName; + const indexRoute = "adminBadges.index"; + if (currentRoute === indexRoute) { + return "adminBadges.show"; + } else { + return this.routing.currentRouteName; + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-badges-award.js.es6 b/app/assets/javascripts/admin/routes/admin-badges-award.js.es6 new file mode 100644 index 0000000000..90a4cba17f --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-badges-award.js.es6 @@ -0,0 +1,12 @@ +import Route from "discourse/routes/discourse"; + +export default Route.extend({ + model(params) { + if (params.badge_id !== "new") { + return this.modelFor("adminBadges").findBy( + "id", + parseInt(params.badge_id, 10) + ); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index f90b66f97a..8297380e05 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -190,6 +190,7 @@ export default function() { "adminBadges", { path: "/badges", resetNamespace: true }, function() { + this.route("award", { path: "/award/:badge_id" }); this.route("show", { path: "/:badge_id" }); } ); diff --git a/app/assets/javascripts/admin/templates/badges-award.hbs b/app/assets/javascripts/admin/templates/badges-award.hbs new file mode 100644 index 0000000000..cf60ff513f --- /dev/null +++ b/app/assets/javascripts/admin/templates/badges-award.hbs @@ -0,0 +1,22 @@ +{{#d-section class="award-badge"}} +
+

{{i18n 'admin.badges.mass_award.title'}}

+
+ {{#if model}} + {{icon-or-image model}} + {{model.name}} + {{else}} + {{I18n 'admin.badges.mass_award.no_badge_selected'}} + {{/if}} +
+
+

{{I18n 'admin.badges.mass_award.upload_csv'}}

+ +
+ {{d-button + class="btn-primary" + action=(action 'massAward') + disabled=saving + label="admin.badges.save"}} +
+{{/d-section}} \ No newline at end of file diff --git a/app/assets/javascripts/admin/templates/badges.hbs b/app/assets/javascripts/admin/templates/badges.hbs index 398a5c08fc..e1ade47b01 100644 --- a/app/assets/javascripts/admin/templates/badges.hbs +++ b/app/assets/javascripts/admin/templates/badges.hbs @@ -6,13 +6,18 @@ {{d-icon "plus"}} {{i18n 'admin.badges.new'}} {{/link-to}} + + {{#link-to 'adminBadges.award' 'new' class="btn btn-primary"}} + {{d-icon "certificate"}} + {{i18n 'admin.badges.mass_award.button'}} + {{/link-to}}