From 24a3b50d4aab632f8d833a2da65b6cb9c32f1bda Mon Sep 17 00:00:00 2001 From: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com> Date: Tue, 25 Aug 2020 11:14:47 -0600 Subject: [PATCH] Add the Ability to Remove Badge Achievements From Users (#9896) [deploy] * Moves badge_achievement-related code from /admin/bagdes to /admin/badge_achievements - Adds an Admin::BadgeAchievements::Controller - Moves #award and #award_badges to Admin::BadgeAchievements::Controller - Removes #award and #award_badges from Admin::Badges::Controller - Moves award.html.erb from /admin/badges to /admin/badge_achievements - Removes badge_achievement routes from /admin/badges - Adds a redirect for /admin/badges/badge_achievements to routes.rb - Cleans up and refactors code in controllers, views, and routes * Add more actions to Admin::BadgeAchievements::Controller and matching routes - Adds an #index and #destory action to the BadgeAchievements::Controller - Adds destroy to the badge_achievements routes - Adjusts redirects for badge_achievements in routes.rb - Removes dead code from the index view for Badges - Refactors the existing code in the index view for BadgeAchievements * Adds an /admin/badge_achievements_spec and cleans up the /admin/badges_spec - Removes the badge_achievement-related tests from badges_spec.rb - Adds an additional test around deleting badge_achievements * Add pagination to badge_achievements index view * Adds badge_achievements to Admin menu items and add comment to routes.rb -Add badge_achievements to the Admin nav bar -Add a comment regarding redirects for badge_achievements * Resolve JS console stacktrace notices and change wording on deletion buttons * Add a Back to Badge Achievements button to the badge award form * Adjust styling to fix failing /admin/badges_spec.rb * Uses SQL paging to optimize scalibility and adds search functionality to index.html.erb - Use SQL paging in Admin::BadgeAchievements::Controller in #index and #award - Add a search by user ID to Badge Achievements index view - Add a limit of 15 badges to be shown on the Badge Achievement index view - Add a User ID column to the Badge Achievement index view * Add Award Badge button back to Badge Achievement index.html.erb * Change @badge to be more explicit and reword warning around badge deletion * Adds award badge button back, rewords success message, and adjusts SQL paging --- .../admin/application_controller.rb | 53 +++++------ .../admin/badge_achievements_controller.rb | 49 ++++++++++ app/controllers/admin/badges_controller.rb | 22 ----- .../admin/badge_achievements/award.html.erb | 21 +++++ .../admin/badge_achievements/index.html.erb | 47 ++++++++++ app/views/admin/badges/award.html.erb | 21 ----- config/routes.rb | 9 +- .../requests/admin/badge_achievements_spec.rb | 92 +++++++++++++++++++ spec/requests/admin/badges_spec.rb | 56 ----------- 9 files changed, 243 insertions(+), 127 deletions(-) create mode 100644 app/controllers/admin/badge_achievements_controller.rb create mode 100644 app/views/admin/badge_achievements/award.html.erb create mode 100644 app/views/admin/badge_achievements/index.html.erb delete mode 100644 app/views/admin/badges/award.html.erb create mode 100644 spec/requests/admin/badge_achievements_spec.rb diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 35ed8df09..78c62538e 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -6,32 +6,33 @@ module Admin # This is used in app/views/admin/shared/_navbar.html.erb to build the # side navbar in alphabetical order. MENU_ITEMS = [ - { name: "articles", controller: "articles" }, - { name: "broadcasts", controller: "broadcasts" }, - { name: "badges", controller: "badges" }, - { name: "chat_channels", controller: "chat_channels" }, - { name: "comments", controller: "comments" }, - { name: "config", controller: "config" }, - { name: "events", controller: "events" }, - { name: "growth", controller: "growth" }, - { name: "listings", controller: "listings" }, - { name: "moderator_actions", controller: "moderator_actions" }, - { name: "mods", controller: "mods" }, - { name: "privileged_reactions", controller: "privileged_reactions" }, - { name: "organizations", controller: "organizations" }, - { name: "pages", controller: "pages" }, - { name: "permissions", controller: "permissions" }, - { name: "podcasts", controller: "podcasts" }, - { name: "config: profile setup", controller: "profile_fields" }, - { name: "reports", controller: "reports" }, - { name: "response_templates", controller: "response_templates" }, - { name: "sponsorships", controller: "sponsorships" }, - { name: "tags", controller: "tags" }, - { name: "tools", controller: "tools" }, - { name: "users", controller: "users" }, - { name: "vault secrets", controller: "secrets" }, - { name: "webhooks", controller: "webhook_endpoints" }, - { name: "welcome", controller: "welcome" }, + { name: "articles", controller: "articles" }, + { name: "broadcasts", controller: "broadcasts" }, + { name: "badges", controller: "badges" }, + { name: "badge_achievements", controller: "badge_achievements" }, + { name: "chat_channels", controller: "chat_channels" }, + { name: "comments", controller: "comments" }, + { name: "config", controller: "config" }, + { name: "events", controller: "events" }, + { name: "growth", controller: "growth" }, + { name: "listings", controller: "listings" }, + { name: "moderator_actions", controller: "moderator_actions" }, + { name: "mods", controller: "mods" }, + { name: "privileged_reactions", controller: "privileged_reactions" }, + { name: "organizations", controller: "organizations" }, + { name: "pages", controller: "pages" }, + { name: "permissions", controller: "permissions" }, + { name: "podcasts", controller: "podcasts" }, + { name: "profile setup", controller: "profile_fields" }, + { name: "reports", controller: "reports" }, + { name: "response_templates", controller: "response_templates" }, + { name: "sponsorships", controller: "sponsorships" }, + { name: "tags", controller: "tags" }, + { name: "tools", controller: "tools" }, + { name: "users", controller: "users" }, + { name: "vault secrets", controller: "secrets" }, + { name: "webhooks", controller: "webhook_endpoints" }, + { name: "welcome", controller: "welcome" }, ].sort_by { |menu_item| menu_item[:name] }.freeze private diff --git a/app/controllers/admin/badge_achievements_controller.rb b/app/controllers/admin/badge_achievements_controller.rb new file mode 100644 index 000000000..5fd001dca --- /dev/null +++ b/app/controllers/admin/badge_achievements_controller.rb @@ -0,0 +1,49 @@ +module Admin + class BadgeAchievementsController < Admin::ApplicationController + layout "admin" + + def index + @q = BadgeAchievement + .includes(:badge) + .includes(:user) + .order(created_at: :desc) + .ransack(params[:q]) + @badge_achievements = @q.result.page(params[:page] || 1).per(15) + end + + def destroy + @badge_achievement = BadgeAchievement.find(params[:id]) + + if @badge_achievement.destroy + flash[:success] = "Badge achievement has been deleted!" + else + flash[:danger] = @badge_achievement.errors_as_sentence + end + redirect_to admin_badge_achievements_path + end + + def award + @all_badges = Badge.all.select(:title, :slug) + end + + def award_badges + raise ArgumentError, "Please choose a badge to award" if permitted_params[:badge].blank? + + usernames = permitted_params[:usernames].downcase.split(/\s*,\s*/) + message = permitted_params[:message_markdown].presence || "Congrats!" + BadgeAchievements::BadgeAwardWorker.perform_async(usernames, permitted_params[:badge], message) + + flash[:success] = "Badges are being rewarded. The task will finish shortly." + redirect_to admin_badge_achievements_path + rescue ArgumentError => e + flash[:danger] = e.message + redirect_to admin_badge_achievements_path + end + + private + + def permitted_params + params.permit(:usernames, :badge, :message_markdown) + end + end +end diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb index ae5f09258..4a253697f 100644 --- a/app/controllers/admin/badges_controller.rb +++ b/app/controllers/admin/badges_controller.rb @@ -38,32 +38,10 @@ module Admin end end - def award_badges - raise ArgumentError, "Please choose a badge to award" if permitted_params[:badge].blank? - - usernames = permitted_params[:usernames].downcase.split(/\s*,\s*/) - message = permitted_params[:message_markdown].presence || "Congrats!" - BadgeAchievements::BadgeAwardWorker.perform_async(usernames, permitted_params[:badge], message) - - flash[:success] = "Badges are being rewarded. The task will finish shortly." - redirect_to admin_badges_path - rescue ArgumentError => e - flash[:danger] = e.message - redirect_to admin_badges_path - end - - def award - @badge = Badge.all - end - private def badge_params params.require(:badge).permit(:title, :slug, :description, :badge_image) end - - def permitted_params - params.permit(:usernames, :badge, :message_markdown) - end end end diff --git a/app/views/admin/badge_achievements/award.html.erb b/app/views/admin/badge_achievements/award.html.erb new file mode 100644 index 000000000..848dd2179 --- /dev/null +++ b/app/views/admin/badge_achievements/award.html.erb @@ -0,0 +1,21 @@ +
+

Award Badges

+
Warning: Usernames are case-sensitive and must be all lowercase! This task will fail silently when supplied with incorrect + usernames.
+<%= form_with(url: admin_badge_achievements_award_badges_path, local: true) do |f| %> +
+ <%= f.label :badge, "Badge:" %> + <%= f.select("badge", @all_badges.map { |badge| [badge.title, badge.slug] }, include_blank: true, class: "form-control") %> +
+
+ <%= f.label :usernames, "Usernames (Comma Delimited, All Lowercase, No @ Symbol Required In Front Of Usernames)*" %> + <%= f.text_area :usernames, placeholder: "username1, username2, username3", required: true, size: "40x10", class: "form-control" %> +
+
+ <%= f.label :message_markdown, "Override Default Message (Supports Markdown)" %> + <%= f.text_area :message_markdown, size: "40x10", class: "form-control" %> +
+<%= f.submit "Award Badges", class: "btn btn-primary mb-4" %> +<%= link_to "Back to Badge Achievements", admin_badge_achievements_path, class: "btn btn-primary float-right" %> +<% end %> + diff --git a/app/views/admin/badge_achievements/index.html.erb b/app/views/admin/badge_achievements/index.html.erb new file mode 100644 index 000000000..37989655b --- /dev/null +++ b/app/views/admin/badge_achievements/index.html.erb @@ -0,0 +1,47 @@ +
+
+
+ Note: If you remove a badge that is automatically rewarded it will simply be re-awarded despite being removed. +
+
+
+
+ <%= link_to "Award Badge", admin_badge_achievements_award_badges_path, class: "btn btn-primary" %> +
+
+
+ <%= search_form_for @q, url: admin_badge_achievements_path, class: "form-inline justify-content-end" do |f| %> + + <%= f.label :user_id_eq, "User ID", class: "sr-only" %> + <%= f.search_field :user_id_eq, placeholder: "User ID", class: "form-control mx-3" %> + + <%= f.submit "Search", class: "btn btn-secondary" %> + <% end %> +
+
+ + + + + + + + + + + <% @badge_achievements.each do |badge_achievement| %> + + + + +
+
+ + + + <% end %> + +
User IDUserBadgeBadge Image
<%= badge_achievement.user.id %><%= badge_achievement.user.username %><%= badge_achievement.badge.title %> + badge image + <%= link_to "Remove", url_for(action: :destroy, id: badge_achievement.id), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger" %>
+
diff --git a/app/views/admin/badges/award.html.erb b/app/views/admin/badges/award.html.erb deleted file mode 100644 index c8341920c..000000000 --- a/app/views/admin/badges/award.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -
-

Award Badges

-
Warning: Usernames are case-sensitive and must be all lowercase! This task will fail silently when supplied with incorrect - usernames.
- <%= form_with(url: admin_badge_achievements_award_badges_path, local: true) do |f| %> -
- <%= f.label :badge, "Badge:" %> - <%= f.select("badge", @badge.map { |badge| [badge.title, badge.slug] }, include_blank: true, class: "form-control") %> -
-
- <%= f.label :usernames, "Usernames (Comma Delimited, All Lowercase, No @ Symbol Required In Front Of Usernames)*" %> - <%= f.text_area :usernames, placeholder: "username1, username2, username3", required: true, size: "40x10", class: "form-control" %> -
-
- <%= f.label :message_markdown, "Override Default Message (Supports Markdown)" %> - <%= f.text_area :message_markdown, size: "40x10", class: "form-control" %> -
- <%= f.submit "Award Badges", class: "btn btn-primary mb-4" %> - <% end %> - -
diff --git a/config/routes.rb b/config/routes.rb index 166632cb5..17442dc21 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,8 +117,13 @@ Rails.application.routes.draw do resources :webhook_endpoints, only: :index resource :config resources :badges, only: %i[index edit update new create] - get "/badge_achievements/award_badges", to: "badges#award" - post "/badge_achievements/award_badges", to: "badges#award_badges" + # These redirects serve as a safegaurd to prevent 404s for any Admins + # who have the old badge_achievement URLs bookmarked. + get "/badges/badge_achievements", to: redirect("/admin/badge_achievements") + get "/badges/badge_achievements/award_badges", to: redirect("/admin/badge_achievements/award_badges") + resources :badge_achievements, only: %i[index destroy] + get "/badge_achievements/award_badges", to: "badge_achievements#award" + post "/badge_achievements/award_badges", to: "badge_achievements#award_badges" resources :secrets, only: %i[index] put "secrets", to: "secrets#update" end diff --git a/spec/requests/admin/badge_achievements_spec.rb b/spec/requests/admin/badge_achievements_spec.rb new file mode 100644 index 000000000..60b56c707 --- /dev/null +++ b/spec/requests/admin/badge_achievements_spec.rb @@ -0,0 +1,92 @@ +require "rails_helper" +require "requests/shared_examples/internal_policy_dependant_request" + +RSpec.describe "/admin/badges", type: :request do + let(:admin) { create(:user, :super_admin) } + let!(:badge) { create(:badge, title: "Not 'Hello, world!'") } + let(:params) do + { + badge: { + title: "Hello, world!", + slug: "greeting-badge", + description: "Awarded to welcoming users", + badge_image: Rack::Test::UploadedFile.new("spec/support/fixtures/images/image1.jpeg", "image/jpeg") + } + } + end + + it_behaves_like "an InternalPolicy dependant request", Badge do + let(:request) { get "/admin/badges" } + end + + describe "POST /admin/badge_achievements/award_badges" do + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:usernames_string) { "#{user.username}, #{user2.username}" } + let(:usernames_array) { [user.username, user2.username] } + + before do + sign_in admin + allow(BadgeAchievements::BadgeAwardWorker).to receive(:perform_async) + end + + it "awards badges" do + allow(BadgeAchievements::BadgeAwardWorker).to receive(:perform_async) + post admin_badge_achievements_award_badges_path, params: { + badge: badge.slug, + usernames: usernames_string, + message_markdown: "Hinder me? Thou fool. No living man may hinder me!" + } + expect(BadgeAchievements::BadgeAwardWorker).to have_received(:perform_async).with( + usernames_array, badge.slug, "Hinder me? Thou fool. No living man may hinder me!" + ) + expect(request.flash[:success]).to include("Badges are being rewarded. The task will finish shortly.") + end + + it "awards badges with default a message" do + allow(BadgeAchievements::BadgeAwardWorker).to receive(:perform_async) + post admin_badge_achievements_award_badges_path, params: { + badge: badge.slug, + usernames: usernames_string, + message_markdown: "" + } + expect(BadgeAchievements::BadgeAwardWorker).to have_received(:perform_async).with(usernames_array, badge.slug, + "Congrats!") + expect(request.flash[:success]).to include("Badges are being rewarded. The task will finish shortly.") + end + + it "does not award a badge and raises an error if a badge is not specified" do + post admin_badge_achievements_award_badges_path, params: { + usernames: usernames_string, + message_markdown: "" + } + expect(BadgeAchievements::BadgeAwardWorker).not_to have_received(:perform_async).with(usernames_array, + badge.slug, "") + end + + it "does not award a badge if the username provided is not lowercase" do + post admin_badge_achievements_award_badges_path, params: { + badge: badge.slug, + usernames: user.username.upcase, + message_markdown: "" + } + expect(BadgeAchievements::BadgeAwardWorker).not_to have_received(:perform_async).with(user.username.upcase, + badge.slug, "") + end + end + + describe "DELETE /admin/badge_achievements/:id" do + let!(:badge_achievement) { create(:badge_achievement) } + + before do + sign_in admin + end + + it "deletes the badge_achievement" do + expect do + delete "/admin/badge_achievements/#{badge_achievement.id}" + end.to change { BadgeAchievement.all.count }.by(-1) + expect(response.body).to redirect_to "/admin/badge_achievements" + end + end +end diff --git a/spec/requests/admin/badges_spec.rb b/spec/requests/admin/badges_spec.rb index 70cb6b131..30099187e 100644 --- a/spec/requests/admin/badges_spec.rb +++ b/spec/requests/admin/badges_spec.rb @@ -19,62 +19,6 @@ RSpec.describe "/admin/badges", type: :request do let(:request) { get "/admin/badges" } end - describe "POST /admin/badge_achievements/award_badges" do - let(:user) { create(:user) } - let(:user2) { create(:user) } - let(:usernames_string) { "#{user.username}, #{user2.username}" } - let(:usernames_array) { [user.username, user2.username] } - - before do - sign_in admin - allow(BadgeAchievements::BadgeAwardWorker).to receive(:perform_async) - end - - it "awards badges" do - allow(BadgeAchievements::BadgeAwardWorker).to receive(:perform_async) - post admin_badge_achievements_award_badges_path, params: { - badge: badge.slug, - usernames: usernames_string, - message_markdown: "Hinder me? Thou fool. No living man may hinder me!" - } - expect(BadgeAchievements::BadgeAwardWorker).to have_received(:perform_async).with( - usernames_array, badge.slug, "Hinder me? Thou fool. No living man may hinder me!" - ) - expect(request.flash[:success]).to include("Badges are being rewarded. The task will finish shortly.") - end - - it "awards badges with default a message" do - allow(BadgeAchievements::BadgeAwardWorker).to receive(:perform_async) - post admin_badge_achievements_award_badges_path, params: { - badge: badge.slug, - usernames: usernames_string, - message_markdown: "" - } - expect(BadgeAchievements::BadgeAwardWorker).to have_received(:perform_async).with(usernames_array, badge.slug, - "Congrats!") - expect(request.flash[:success]).to include("Badges are being rewarded. The task will finish shortly.") - end - - it "does not award a badge and raises an error if a badge is not specified" do - post admin_badge_achievements_award_badges_path, params: { - usernames: usernames_string, - message_markdown: "" - } - expect(BadgeAchievements::BadgeAwardWorker).not_to have_received(:perform_async).with(usernames_array, - badge.slug, "") - end - - it "does not award a badge if the username provided is not lowercase" do - post admin_badge_achievements_award_badges_path, params: { - badge: badge.slug, - usernames: user.username.upcase, - message_markdown: "" - } - expect(BadgeAchievements::BadgeAwardWorker).not_to have_received(:perform_async).with(user.username.upcase, - badge.slug, "") - end - end - describe "POST /admin/badges" do let(:post_resource) { post "/admin/badges", params: params }