Skip to content

Commit

Permalink
Allow admins to set max score for some users (forem#21203)
Browse files Browse the repository at this point in the history
* Allow admins to set max score for some users

* Add keys
  • Loading branch information
benhalpern authored Aug 15, 2024
1 parent 3398389 commit e624f65
Show file tree
Hide file tree
Showing 16 changed files with 184 additions and 5 deletions.
25 changes: 25 additions & 0 deletions app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class UsersController < Admin::ApplicationController
organization_id identity_id
credit_action credit_amount
reputation_modifier
max_score
tag_name
].freeze

Expand Down Expand Up @@ -104,6 +105,30 @@ def reputation_modifier
redirect_to admin_user_path(@user)
end

def max_score
@user = User.find(params[:id])
max_score_value = user_params[:max_score]
note_content = if user_params[:new_note].present?
"Changed user's maximum score to #{max_score_value}. " \
"Reason: #{user_params[:new_note]}"
else
"Changed user's maximum score to #{max_score_value}."
end
if @user.update(max_score: max_score_value)
Note.create(
author_id: current_user.id,
noteable_id: @user.id,
noteable_type: "User",
reason: "max_score_change",
content: note_content,
)
flash[:success] = I18n.t("views.admin.users.max_score.success", max_score: max_score_value)
else
flash[:error] = I18n.t("views.admin.users.max_score.error")
end
redirect_to admin_user_path(@user)
end

def destroy
role = Role.find(params[:role_id])
authorize(role, :remove_role?)
Expand Down
5 changes: 4 additions & 1 deletion app/models/article.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def self.unique_url_error
validates :video_state, inclusion: { in: %w[PROGRESSING COMPLETED] }, allow_nil: true
validates :video_thumbnail_url, url: { allow_blank: true, schemes: %w[https http] }
validates :clickbait_score, numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 1.0 }
validates :max_score, numericality: { greater_than_or_equal_to: 0 }
validate :future_or_current_published_at, on: :create
validate :correct_published_at?, on: :update, unless: :admin_update

Expand Down Expand Up @@ -597,7 +598,9 @@ def update_score
spam_adjustment = user.spam? ? -500 : 0
negative_reaction_adjustment = Reaction.where(reactable_id: user_id, reactable_type: "User").sum(:points)
self.score = reactions.sum(:points) + spam_adjustment + negative_reaction_adjustment + base_subscriber_adjustment
self.score = max_score if max_score.positive? && max_score < score
accepted_max = [max_score, user&.max_score.to_i].min
accepted_max = [max_score, user&.max_score.to_i].max if accepted_max.zero?
self.score = accepted_max if accepted_max.positive? && accepted_max < score

update_columns(score: score,
privileged_users_reaction_points_sum: reactions.privileged_category.sum(:points),
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class User < ApplicationRecord
validates :spent_credits_count, presence: true
validates :subscribed_to_user_subscriptions_count, presence: true
validates :unspent_credits_count, presence: true
validates :max_score, numericality: { greater_than_or_equal_to: 0 }
validates :reputation_modifier, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 5 },
presence: true

Expand Down
5 changes: 4 additions & 1 deletion app/views/admin/users/show/emails/_verification.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
<h2 class="crayons-subtitle-1"><%= t("views.admin.users.emails.not_verified.subtitle") %></h2>
<p class="color-secondary"><%= t("views.admin.users.emails.not_verified.desc", user: @user.name) %></p>
</div>
<%= f.button t("views.admin.users.emails.verify"), class: "c-btn c-btn--secondary whitespace-nowrap" %>
<% if false # Not yet designed to be visible %>
<%= f.button t("views.admin.users.emails.manually_verify"), class: "c-btn c-btn--secondary whitespace-nowrap", value: "manual_verify" %>
<% end %>
<%= f.button t("views.admin.users.emails.verify"), class: "c-btn c-btn--secondary whitespace-nowrap", value: "send_verification_email" %>
<% else %>
<p>
<%= t("views.admin.users.emails.verified_html", time: tag.time(l(@last_email_verification_date, format: :email_verified), datetime: @last_email_verification_date.strftime("%Y-%m-%dT%H:%M:%S%z"), class: "fw-medium whitespace-nowrap")) %>
Expand Down
2 changes: 2 additions & 0 deletions app/views/admin/users/show/profile/_actions.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<li><button type="button" class="c-btn w-100 align-left" data-modal-title="<%= t("views.admin.users.social.heading") %>" data-modal-size="medium" data-modal-content-selector="#remove-social-accounts"><%= t("views.admin.users.profile.options.remove_social") %></button></li>
<% end %>
<li><button type="button" class="c-btn w-100 align-left" data-modal-title="<%= t("views.admin.users.reputation.heading") %>" data-modal-size="medium" data-modal-content-selector="#change-reputation"><%= t("views.admin.users.reputation.change") %></button></li>
<li><button type="button" class="c-btn w-100 align-left" data-modal-title="<%= t("views.admin.users.max_score.heading") %>" data-modal-size="medium" data-modal-content-selector="#change-max-score"><%= t("views.admin.users.max_score.change") %></button></li>
<li>
<button type="button"
class="c-btn c-btn--destructive w-100 align-left"
Expand All @@ -53,6 +54,7 @@
<%= render "admin/users/modals/unpublish_modal" %>
<%= render "admin/users/show/profile/actions/social_accounts" %>
<%= render "admin/users/show/profile/actions/change_reputation" %>
<%= render "admin/users/show/profile/actions/change_max_score" %>
<%= render "admin/users/modals/banish_modal" %>
<%= render "admin/users/show/profile/actions/delete" %>
</div>
4 changes: 4 additions & 0 deletions app/views/admin/users/show/profile/_status.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
<% else %>
<span data-testid="user-status" class="c-indicator c-indicator--relaxed"><%= t("views.admin.users.statuses.Good standing") %></span>
<% end %>
<% if user.max_score > 0 %>
<span class="screen-reader-only"><%= t("views.admin.users.max_score_reader") %></span>
<span data-testid="user-max-score" class="c-indicator c-indicator--relaxed">Max: <%= user.max_score %></span>
<% end %>
</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div id="change-max-score">
<%= form_for(@user, url: max_score_admin_user_path(@user),
html: { class: "flex flex-col gap-4", method: :patch }) do |f| %>
<p><%= t("views.admin.users.max_score.desc_html", max_score: @user.max_score) %></p>
<div class="crayons-field mb-4">
<%= f.label :max_score, t("views.admin.users.max_score.max_score"), class: "crayons-field__label" %>
<%= f.text_field :max_score, placeholder: @user.max_score, class: "crayons-textfield", type: "number", inputmode: "numeric", step: "1", min: 0, aria: { describedby: "reputation_modifier" } %>
</div>
<div class="crayons-field mb-4">
<%= f.label :new_note, t("views.admin.users.max_score.change_note"), class: "crayons-field__label" %>
<%= f.text_area :new_note, size: 50, required: true, class: "crayons-textfield", id: "new_note" %>
</div>
<div>
<%= f.button t("views.admin.users.max_score.submit"), class: "c-btn c-btn--primary", type: "submit" %>
</div>
<% end %>
</div>
1 change: 1 addition & 0 deletions config/locales/controllers/admin/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ en:
email_sent: Email sent!
confirm_sent: Confirmation email sent!
verify_sent: Verification email sent!
manual_verification_sent: Email manually verified!
full_delete_html: '@%{user} (email: %{email}, user_id: %{id}) has been fully deleted. If this is a GDPR delete, delete them from Mailchimp & Google Analytics and confirm on %{the_page}.'
no_email: no email
parameter_missing: Both subject and body are required!
Expand Down
1 change: 1 addition & 0 deletions config/locales/controllers/admin/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ fr:
email_sent: Courriel envoyé !
confirm_sent: Courriel de Confirmation Envoyé!
verify_sent: E-mail de vérification envoyé !
manual_verification_sent: E-mail vérifié manuellement !
full_delete_html: "@%{user} (email: %{email}, user_id: %{id}) a été entièrement supprimé. S'il s'agit d'une suppression GDPR, supprimez-les de Mailchimp & Google Analytics et confirmez sur %{the_page}."
no_email: Aucun e-mail
parameter_missing: Le sujet et le corps sont tous deux requis !
Expand Down
14 changes: 13 additions & 1 deletion config/locales/views/admin/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ en:
not_verified:
subtitle: Email not verified
desc: "%{user}'s email hasn't been verified yet."
verify: Verify email address
verify: Send new verification email email
manually_verify: Manually verify email
verified_html: Last verified on %{time}
reverify: Reverify
flags:
Expand Down Expand Up @@ -378,6 +379,15 @@ en:
submit: Change reputation modifier
success: Successfully changed user's reputation modifier to %{reputation_modifier}.
error: Failed to change user's reptuation modifier (select between 0 and 5)
max_score:
change: Change maximum score
change_note: Reason for change
desc_html: This changes the maximum score a user can achieve. 0 means unlimited. The user's current maximum score is <strong>%{max_score}</strong>.
heading: Change maximum score
max_score: New maximum score
submit: Change maximum score
success: Successfully changed user's maximum score to %{max_score}.
error: Failed to change user's maximum score (select between 0 and 100)
unpublish_logs:
subtitle_html: Unpublished by %{user} on %{time}
posts: "Posts:"
Expand Down Expand Up @@ -426,6 +436,8 @@ en:
Good Standing: Good Standing
Good standing: Good standing
Trusted: Trusted
max_score_title: Maximum score
max_score_reader: "Maximum score:"
identities:
facebook: Facebook
github: GitHub
Expand Down
14 changes: 13 additions & 1 deletion config/locales/views/admin/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ fr:
not_confirmed:
subtitle: E-mail non confirmé
desc: "L'e-mail de %{user} n'a pas encore été confirmé."
verify: Verify email address
verify: Envoyer un nouvel e-mail de vérification
manually_verify: Vérifier manuellement l'e-mail
verified_html: Last verified on %{time}
reverify: Reverify
flags:
Expand Down Expand Up @@ -375,6 +376,15 @@ fr:
submit: Changer le modificateur de réputation
success: Modification réussie du modificateur de réputation de l'utilisateur en %{reputation_modifier}.
error: Échec de la modification du modificateur de réputation de l'utilisateur (sélectionnez entre 0 et 5).
max_score:
change: Changer le score maximum
change_note: Raison du changement
desc_html: Cela modifie le score maximum qu'un utilisateur peut atteindre. 0 signifie illimité. Le score maximum actuel de l'utilisateur est <strong>%{max_score}</strong>.
heading: Changer le score maximum
max_score: Nouveau score maximum
submit: Changer le score maximum
success: Modification réussie du score maximum de l'utilisateur en %{max_score}.
error: Échec de la modification du score maximum de l'utilisateur (sélectionnez entre 0 et 100)
unpublish_logs:
subtitle_html: Unpublished by %{user} on %{time}
posts: "Posts:"
Expand Down Expand Up @@ -423,6 +433,8 @@ fr:
Good Standing: Good Standing
Good standing: Good standing
Trusted: Trusted
max_score_title: Maximum score
max_score_reader: "Maximum score:"
identities:
facebook: Facebook
github: GitHub
Expand Down
1 change: 1 addition & 0 deletions config/routes/admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
member do
post "banish"
patch "reputation_modifier"
patch "max_score"
post "export_data"
post "full_delete"
patch "user_status"
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20240814184735_add_max_score_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddMaxScoreToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :max_score, :integer, default: 0
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2024_07_10_144409) do
ActiveRecord::Schema[7.0].define(version: 2024_08_14_184735) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "ltree"
Expand Down Expand Up @@ -1313,6 +1313,7 @@
t.inet "last_sign_in_ip"
t.datetime "latest_article_updated_at", precision: nil
t.datetime "locked_at", precision: nil
t.integer "max_score", default: 0
t.string "name"
t.string "old_old_username"
t.string "old_username"
Expand Down
18 changes: 18 additions & 0 deletions spec/models/article_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,24 @@ def foo():
expect(article.reload.score).to eq(10)
end
end

context "when user.max_score is set" do
it "uses the user's max score if it is lower than the article's max score" do
user.update_column(:max_score, 5)
article.update_column(:max_score, 10)

article.update_score
expect(article.reload.score).to eq(5)
end

it "uses the article's max score if it is lower than the user's max score" do
user.update_column(:max_score, 10)
article.update_column(:max_score, 5)

article.update_score
expect(article.reload.score).to eq(5)
end
end
end

describe "#feed_source_url and canonical_url must be unique for published articles" do
Expand Down
73 changes: 73 additions & 0 deletions spec/requests/admin/users/users_change_max_score_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
require "rails_helper"

RSpec.describe "/admin/member_manager/users" do
let!(:user) { create(:user) }
let!(:admin) { create(:user, :super_admin) }

before do
sign_in(admin)
end

describe "PATCH /admin/member_manager/users/:id/max_score" do
let(:new_max_score) { 500 }
let(:note_content) { "Increased due to high performance" }

it "updates the user's max score" do
patch max_score_admin_user_path(user.id), params: {
user: {
max_score: new_max_score,
new_note: note_content
}
}

user.reload
expect(user.max_score).to eq(new_max_score)
expect(flash[:success]).to be_present
end

it "creates a note with the reason for the change" do
expect do
patch max_score_admin_user_path(user.id), params: {
user: {
max_score: new_max_score,
new_note: note_content
}
}
end.to change(Note, :count).by(1)

note = Note.last
expect(note.content).to include("Changed user's maximum score to #{new_max_score}")
expect(note.content).to include("Reason: #{note_content}")
expect(note.reason).to eq("max_score_change")
expect(note.author_id).to eq(admin.id)
expect(note.noteable_id).to eq(user.id)
end

it "redirects to the user admin page" do
patch max_score_admin_user_path(user.id), params: {
user: {
max_score: new_max_score,
new_note: ""
}
}

expect(response).to redirect_to(admin_user_path(user))
end

context "when the update fails" do
let(:invalid_max_score) { -2 }

it "sets a flash error message" do
patch max_score_admin_user_path(user.id), params: {
user: {
max_score: invalid_max_score,
new_note: note_content
}
}

expect(flash[:error]).to be_present
expect(response).to redirect_to(admin_user_path(user))
end
end
end
end

0 comments on commit e624f65

Please sign in to comment.