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
This commit is contained in:
Julianna Tetreault 2020-08-25 11:14:47 -06:00 committed by GitHub
parent b0bacf60fb
commit 24a3b50d4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 243 additions and 127 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,21 @@
<main>
<h2>Award Badges</h2>
<div class="alert alert-warning"><strong>Warning: Usernames are case-sensitive and must be all lowercase! This task will fail silently when supplied with incorrect
usernames.<strong></div>
<%= form_with(url: admin_badge_achievements_award_badges_path, local: true) do |f| %>
<div class="form-group">
<%= f.label :badge, "Badge:" %>
<%= f.select("badge", @all_badges.map { |badge| [badge.title, badge.slug] }, include_blank: true, class: "form-control") %>
</div>
<div class="form-group">
<%= 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" %>
</div>
<div class="form-group">
<%= f.label :message_markdown, "Override Default Message (Supports Markdown)" %>
<%= f.text_area :message_markdown, size: "40x10", class: "form-control" %>
</div>
<%= 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 %>
</div>

View file

@ -0,0 +1,47 @@
<main>
<br>
<div class="alert alert-warning">
<strong>Note: If you remove a badge that is automatically rewarded it will simply be re-awarded despite being removed.</strong>
</div>
<div class="row m-3">
<div class="col">
<div class="row justify-content-right">
<%= link_to "Award Badge", admin_badge_achievements_award_badges_path, class: "btn btn-primary" %>
</div>
</div>
<div class="col">
<%= 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 %>
</div>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">User ID</th>
<th scope="col">User</th>
<th scope="col">Badge</th>
<th scope="col">Badge Image</th>
</tr>
</thead>
<tbody>
<% @badge_achievements.each do |badge_achievement| %>
<tr>
<td><%= badge_achievement.user.id %></td>
<td><%= badge_achievement.user.username %></td>
<td><%= badge_achievement.badge.title %></td>
<h5>
<td class="justify-content-center">
<img class="mx-auto mt-3" width="40" height="40" src="<%= badge_achievement.badge.badge_image %>" alt="badge image">
</td>
<td><%= link_to "Remove", url_for(action: :destroy, id: badge_achievement.id), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger" %></td>
</h5>
</tr>
<% end %>
</tbody>
</table>
</main>

View file

@ -1,21 +0,0 @@
<main>
<h2>Award Badges</h2>
<div class="alert alert-warning"><strong>Warning: Usernames are case-sensitive and must be all lowercase! This task will fail silently when supplied with incorrect
usernames.<strong></div>
<%= form_with(url: admin_badge_achievements_award_badges_path, local: true) do |f| %>
<div class="form-group">
<%= f.label :badge, "Badge:" %>
<%= f.select("badge", @badge.map { |badge| [badge.title, badge.slug] }, include_blank: true, class: "form-control") %>
</div>
<div class="form-group">
<%= 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" %>
</div>
<div class="form-group">
<%= f.label :message_markdown, "Override Default Message (Supports Markdown)" %>
<%= f.text_area :message_markdown, size: "40x10", class: "form-control" %>
</div>
<%= f.submit "Award Badges", class: "btn btn-primary mb-4" %>
<% end %>
</div>
</main>

View file

@ -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

View file

@ -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

View file

@ -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 }