Skip to content

Commit ab4601a

Browse files
authored
Add ability for admin to edit user profile fields (with note auditing) (forem#22738)
* Add ability for admin to edit user profile fields (with note auditing) * Fix profile in spec
1 parent 83b2aee commit ab4601a

File tree

8 files changed

+167
-0
lines changed

8 files changed

+167
-0
lines changed

app/controllers/admin/users_controller.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class UsersController < Admin::ApplicationController
1515
email
1616
].freeze
1717

18+
ADMIN_PROFILE_USER_PARAMS = %i[name username].freeze
19+
ADMIN_PROFILE_PROFILE_PARAMS = %i[summary location website_url].freeze
20+
1821
EMAIL_ALLOWED_PARAMS = %i[
1922
email_subject
2023
email_body
@@ -128,6 +131,31 @@ def update_email
128131
redirect_to admin_user_path(@user)
129132
end
130133

134+
def update_profile
135+
@user = User.find(params[:id])
136+
previous_user_values = @user.slice(*ADMIN_PROFILE_USER_PARAMS)
137+
previous_profile_values = (@user.profile || @user.build_profile).slice(*ADMIN_PROFILE_PROFILE_PARAMS)
138+
139+
update_result = Users::Update.call(@user,
140+
user: admin_profile_user_params,
141+
profile: admin_profile_params)
142+
143+
if update_result.success?
144+
Note.create(
145+
author_id: current_user.id,
146+
noteable_id: @user.id,
147+
noteable_type: "User",
148+
reason: "admin_profile_update",
149+
content: profile_update_note(previous_user_values, previous_profile_values),
150+
)
151+
flash[:success] = I18n.t("views.admin.users.edit_profile.success")
152+
else
153+
flash[:error] = update_result.errors_as_sentence
154+
end
155+
156+
redirect_to admin_user_path(@user)
157+
end
158+
131159
def max_score
132160
@user = User.find(params[:id])
133161
max_score_value = user_params[:max_score]
@@ -580,6 +608,42 @@ def credit_params
580608
credit_params
581609
end
582610

611+
def admin_profile_user_params
612+
params.require(:user).permit(ADMIN_PROFILE_USER_PARAMS)
613+
end
614+
615+
def admin_profile_params
616+
params.fetch(:profile, {}).permit(ADMIN_PROFILE_PROFILE_PARAMS)
617+
end
618+
619+
def profile_update_note(previous_user_values, previous_profile_values)
620+
changes = []
621+
updated_user_values = @user.slice(*ADMIN_PROFILE_USER_PARAMS)
622+
updated_profile_values = (@user.profile || @user.build_profile).slice(*ADMIN_PROFILE_PROFILE_PARAMS)
623+
624+
append_profile_change(changes, "name", previous_user_values["name"], updated_user_values["name"])
625+
append_profile_change(changes, "username", previous_user_values["username"], updated_user_values["username"])
626+
append_profile_change(changes, "summary", previous_profile_values["summary"], updated_profile_values["summary"])
627+
append_profile_change(changes, "location", previous_profile_values["location"], updated_profile_values["location"])
628+
append_profile_change(changes, "website_url", previous_profile_values["website_url"], updated_profile_values["website_url"])
629+
630+
if changes.empty?
631+
"Admin #{current_user.username} submitted a profile update with no changes detected."
632+
else
633+
"Admin #{current_user.username} updated profile fields: #{changes.join('; ')}"
634+
end
635+
end
636+
637+
def append_profile_change(changes, label, previous_value, updated_value)
638+
return if previous_value == updated_value
639+
640+
changes << "#{label}: #{format_profile_value(previous_value)} -> #{format_profile_value(updated_value)}"
641+
end
642+
643+
def format_profile_value(value)
644+
value.present? ? "'#{value}'" : "(blank)"
645+
end
646+
583647
def set_current_tab(current_tab = "overview")
584648
@current_tab = if current_tab.in? Constants::UserDetails::TAB_LIST.map(&:underscore)
585649
current_tab

app/views/admin/users/show/profile/_actions.html.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<% if @user.access_locked? %>
1414
<li><%= link_to t("views.admin.users.profile.locked.unlock"), unlock_access_admin_user_path(@user), method: :patch, class: "c-link c-link--block" %></li>
1515
<% end %>
16+
<li><button type="button" class="c-btn w-100 align-left" data-modal-title="<%= t("views.admin.users.edit_profile.heading", user: @user.name) %>" data-modal-size="medium" data-modal-content-selector="#edit-profile"><%= t("views.admin.users.profile.options.edit_profile") %></button></li>
1617
<li><button type="button" class="c-btn w-100 align-left" data-modal-title="<%= t("views.admin.users.update_email.heading", user: @user.name) %>" data-modal-size="medium" data-modal-content-selector="#update-email"><%= t("views.admin.users.profile.options.update_email") %></button></li>
1718
<li><button type="button" class="c-btn w-100 align-left" data-modal-title="<%= t("views.admin.users.export.heading", user: @user.name) %>" data-modal-size="small" data-modal-content-selector="#export-data"><%= t("views.admin.users.profile.options.export") %></button></li>
1819
<li><button type="button" class="c-btn w-100 align-left" data-modal-title="<%= t("views.admin.users.merge.heading") %>" data-modal-size="small" data-modal-content-selector="#merge-accounts"><%= t("views.admin.users.profile.options.merge") %></button></li>
@@ -51,6 +52,7 @@
5152
<%# These are contents for modals %>
5253
<div class="hidden">
5354
<%= render "admin/users/show/profile/actions/export" %>
55+
<%= render "admin/users/show/profile/actions/edit_profile" %>
5456
<%= render "admin/users/show/profile/actions/update_email" %>
5557
<%= render "admin/users/show/profile/actions/merge" %>
5658
<%= render "admin/users/modals/unpublish_modal" %>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<div id="edit-profile">
2+
<%= form_for(@user, url: update_profile_admin_user_path(@user),
3+
html: { class: "flex flex-col gap-4", method: :patch }) do |f| %>
4+
<div class="crayons-notice crayons-notice--warning">
5+
<p><%= t("views.admin.users.edit_profile.notice") %></p>
6+
</div>
7+
<p><%= t("views.admin.users.edit_profile.desc_html", user: @user.name) %></p>
8+
<div class="crayons-field">
9+
<%= f.label :name, t("views.admin.users.edit_profile.name"), class: "crayons-field__label" %>
10+
<%= f.text_field :name, class: "crayons-textfield", required: true %>
11+
</div>
12+
<div class="crayons-field">
13+
<%= f.label :username, t("views.admin.users.edit_profile.username"), class: "crayons-field__label" %>
14+
<%= f.text_field :username, class: "crayons-textfield", required: true %>
15+
</div>
16+
<%= fields_for :profile, (@user.profile || @user.build_profile) do |pf| %>
17+
<div class="crayons-field">
18+
<%= pf.label :summary, t("views.admin.users.edit_profile.summary"), class: "crayons-field__label" %>
19+
<%= pf.text_area :summary, class: "crayons-textfield", rows: 4 %>
20+
</div>
21+
<div class="crayons-field">
22+
<%= pf.label :location, t("views.admin.users.edit_profile.location"), class: "crayons-field__label" %>
23+
<%= pf.text_field :location, class: "crayons-textfield" %>
24+
</div>
25+
<div class="crayons-field">
26+
<%= pf.label :website_url, t("views.admin.users.edit_profile.website_url"), class: "crayons-field__label" %>
27+
<%= pf.url_field :website_url, class: "crayons-textfield" %>
28+
</div>
29+
<% end %>
30+
<div>
31+
<%= f.button t("views.admin.users.edit_profile.submit"), class: "c-btn c-btn--primary", type: "submit" %>
32+
</div>
33+
<% end %>
34+
</div>

config/locales/views/admin/en.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,13 +356,25 @@ en:
356356
visit: Visit profile
357357
options:
358358
icon: Options
359+
edit_profile: Edit profile
359360
update_email: Update User Email
360361
export: Export data
361362
merge: Merge users
362363
unpublish: Unpublished all posts
363364
remove_social: Remove social accounts
364365
banish: Banish user
365366
delete: Delete user
367+
edit_profile:
368+
heading: Edit %{user}'s profile
369+
desc_html: Changes you make here are immediately public on the user's profile.
370+
notice: Be careful. These updates are public-facing and affect the user's account.
371+
name: Name
372+
username: Username
373+
summary: Summary
374+
location: Location
375+
website_url: Website URL
376+
submit: Save profile changes
377+
success: Profile updated successfully.
366378
reports:
367379
subtitle: Reports submitted by %{user} (%{num})
368380
for_html: For %{url}

config/locales/views/admin/fr.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,24 @@ fr:
346346
visit: Visit profile
347347
options:
348348
icon: Options
349+
edit_profile: Modifier le profil
349350
export: Export data
350351
merge: Merge users
351352
unpublish: Unpublished all posts
352353
remove_social: Remove social accounts
353354
banish: Banish user
354355
delete: Delete user
356+
edit_profile:
357+
heading: Modifier le profil de %{user}
358+
desc_html: Les changements faits ici sont immédiatement publics sur le profil de l'utilisateur.
359+
notice: Soyez prudent. Ces mises à jour sont visibles publiquement et affectent le compte de l'utilisateur.
360+
name: Nom
361+
username: Nom d'utilisateur
362+
summary: Résumé
363+
location: Localisation
364+
website_url: URL du site
365+
submit: Enregistrer les modifications du profil
366+
success: Profil mis à jour avec succès.
355367
reports:
356368
subtitle: Reports submitted by %{user} (%{num})
357369
for_html: For %{url}

config/locales/views/admin/pt.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ pt:
3737
email: E-mail
3838
username: Nome de usuário
3939
name: Nome
40+
profile:
41+
options:
42+
edit_profile: Editar perfil
43+
edit_profile:
44+
heading: Editar perfil de %{user}
45+
desc_html: As mudanças feitas aqui ficam imediatamente públicas no perfil do usuário.
46+
notice: Tenha cuidado. Essas atualizações são públicas e afetam a conta do usuário.
47+
name: Nome
48+
username: Nome de usuário
49+
summary: Resumo
50+
location: Localização
51+
website_url: URL do site
52+
submit: Salvar alterações do perfil
53+
success: Perfil atualizado com sucesso.
4054
bio: Biografia
4155
location: Localização
4256
website: Site

config/routes/admin.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
post "banish"
5252
patch "reputation_modifier"
5353
patch "max_score"
54+
patch "update_profile"
5455
patch "update_email"
5556
post "export_data"
5657
post "full_delete"

spec/requests/admin/users_manage_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,34 @@ def full_profile
175175
expect(Note.last.content).to eq("general note about whatever")
176176
end
177177

178+
it "updates profile fields and records a note" do
179+
user.profile.update!(
180+
summary: "Old summary",
181+
location: "Old location",
182+
website_url: "https://old.example.com",
183+
)
184+
185+
patch update_profile_admin_user_path(user.id), params: {
186+
user: { name: "New Name", username: "newuserhandle" },
187+
profile: { summary: "New summary", location: "New location", website_url: "https://new.example.com" }
188+
}
189+
190+
user.reload
191+
192+
expect(user.name).to eq("New Name")
193+
expect(user.username).to eq("newuserhandle")
194+
expect(user.profile.summary).to eq("New summary")
195+
expect(user.profile.location).to eq("New location")
196+
expect(user.profile.website_url).to eq("https://new.example.com")
197+
expect(Note.last.reason).to eq("admin_profile_update")
198+
expect(Note.last.content).to include("Admin #{super_admin.username} updated profile fields")
199+
expect(Note.last.content).to include("name:")
200+
expect(Note.last.content).to include("username:")
201+
expect(Note.last.content).to include("summary:")
202+
expect(Note.last.content).to include("location:")
203+
expect(Note.last.content).to include("website_url:")
204+
end
205+
178206
it "remove credits from account" do
179207
create_list(:credit, 5, user: user)
180208
put admin_user_path(user.id), params: { user: { remove_credits: "3" } }

0 commit comments

Comments
 (0)