diff --git a/app/assets/stylesheets/tabs.scss b/app/assets/stylesheets/tabs.scss index 108168625..ce2c1345a 100644 --- a/app/assets/stylesheets/tabs.scss +++ b/app/assets/stylesheets/tabs.scss @@ -2,4 +2,8 @@ .tabs { margin-bottom: 1em; -} \ No newline at end of file + + .tabs--push { + flex-grow: 1; + } +} diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss index 90d9fadc2..7dc2ce359 100644 --- a/app/assets/stylesheets/users.scss +++ b/app/assets/stylesheets/users.scss @@ -241,3 +241,18 @@ $sizes: (16, 32, 40, 48, 64, 128, 256); } } } + +.modtools--sidebar { + margin-right: 1rem; +} +.modtools--usercard { + padding: 0.5rem; +} +.modtools-tbl-noborder { + th { + border-bottom-width: 1px !important; + width: 150px; + } +} + +.mod-warnings-clear-form { display: inline; } diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index f17661088..b5aace580 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -2,9 +2,9 @@ class AdminController < ApplicationController before_action :verify_admin, except: [:change_back, :verify_elevation] before_action :verify_global_admin, only: [:admin_email, :send_admin_email, :new_site, :create_site, :setup, - :setup_save, :hellban, :all_email, :send_all_email] + :setup_save, :failban, :all_email, :send_all_email] before_action :verify_developer, only: [:change_users, :impersonate] - before_action :set_user, only: [:change_users, :hellban, :impersonate] + before_action :set_user, only: [:change_users, :failban, :impersonate] skip_before_action :check_if_warning_or_suspension_pending, only: [:change_back, :verify_elevation] @@ -204,7 +204,7 @@ def setup_save render end - def hellban + def failban @user.block("user manually blocked by admin ##{current_user.id}") flash[:success] = t 'admin.user_fed_stat' redirect_back fallback_location: admin_path diff --git a/app/controllers/moderator_controller.rb b/app/controllers/moderator_controller.rb index c2f649e7f..60e5ee4fe 100644 --- a/app/controllers/moderator_controller.rb +++ b/app/controllers/moderator_controller.rb @@ -69,6 +69,7 @@ def user_vote_summary total: Vote.for(@user).count ) ) + render layout: 'without_sidebar' end def spammy_users diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 9a81f07bc..38ba9eb29 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -2,6 +2,8 @@ # rubocop:disable Metrics/ClassLength class UsersController < ApplicationController + layout 'without_sidebar' + include Devise::Controllers::Rememberable before_action :authenticate_user!, only: [:edit_profile, :update_profile, :stack_redirect, @@ -12,11 +14,12 @@ class UsersController < ApplicationController before_action :redirect_to_sign_in, only: [:filters], unless: [:user_signed_in?, :json_request?] - before_action :verify_moderator, only: [:mod, :destroy, :soft_delete, :role_toggle, :full_log, - :annotate, :annotations, :mod_privileges, :mod_privilege_action] - before_action :set_user, only: [:show, :mod, :destroy, :soft_delete, :posts, :role_toggle, :full_log, :activity, - :annotate, :annotations, :mod_privileges, :mod_privilege_action, - :vote_summary, :network, :avatar] + before_action :verify_moderator, only: [:mod, :soft_delete, :role_toggle, :full_log, + :annotate, :annotations, :mod_privileges, :mod_privilege_action, :mod_delete] + before_action :verify_global_moderator, only: [:mod_failban, :mod_delete_network_account] + before_action :set_user, only: [:show, :mod, :mod_delete, :mod_failban, :mod_delete_network_account, :soft_delete, + :posts, :role_toggle, :full_log, :activity, :annotate, :annotations, :mod_privileges, + :mod_privilege_action, :vote_summary, :network, :avatar] before_action :check_deleted, only: [:show, :posts, :activity] def index @@ -36,7 +39,9 @@ def index @post_counts = Post.where(user_id: @users.pluck(:id).uniq).group(:user_id).count respond_to do |format| - format.html + format.html do + render layout: 'application' + end format.json do render json: @users end @@ -60,7 +65,6 @@ def show .count end @posts = @posts.first(@limit) - render layout: 'without_sidebar' end def me @@ -92,7 +96,6 @@ def preferences prefs = current_user.preferences @preferences = prefs[:global] @community_prefs = prefs[:community] - render layout: 'without_sidebar' end format.json do render json: current_user.preferences @@ -135,9 +138,7 @@ def filters_json def filters respond_to do |format| - format.html do - render layout: 'without_sidebar' - end + format.html format.json do render json: filters_json end @@ -225,7 +226,7 @@ def posts respond_to do |format| format.html do - render :posts + render :posts, layout: 'application' end format.json do render json: @posts @@ -239,7 +240,6 @@ def my_network def network @communities = Community.all - render layout: 'without_sidebar' end def my_activity @@ -272,11 +272,8 @@ def activity end @items = items.sort_by(&:created_at).reverse.paginate(page: params[:page], per_page: 50) - render layout: 'without_sidebar' end - def mod; end - def full_log @posts = Post.by(@user).count @comments = Comment.by(@user).count @@ -315,8 +312,6 @@ def full_log SuggestedEdit.by(@user).all + PostHistory.by(@user).all + ModWarning.to(@user).all end).sort_by(&:created_at).reverse.paginate(page: params[:page], per_page: 50) - - render layout: 'without_sidebar' end def mod_privileges @@ -349,10 +344,6 @@ def soft_delete render json: { status: 'success', user: @user.id } end - def edit_profile - render layout: 'without_sidebar' - end - def cleaned_profile_websites(profile_params) sites = profile_params[:user_websites_attributes] @@ -575,7 +566,6 @@ def annotations @logs = AuditLog.where(log_type: 'user_annotation', related: @user) .newest_first .paginate(page: params[:page], per_page: 20) - render layout: 'without_sidebar' end def annotate @@ -605,8 +595,6 @@ def vote_summary [k, vl.group_by(&:post), vl.sum { |v| v.vote_type * v.vote_count }] end .paginate(page: params[:page], per_page: 15) - - render layout: 'without_sidebar' end def avatar @@ -628,10 +616,6 @@ def specific_avatar end end - def disconnect_sso - render layout: 'without_sidebar' - end - def confirm_disconnect_sso if current_user.sso_profile.blank? || !helpers.devise_sign_in_enabled? || !SiteSetting['AllowSsoDisconnect'] flash[:danger] = 'You cannot disable Single Sign-On.' diff --git a/app/views/mod_warning/log.html.erb b/app/views/mod_warning/log.html.erb index 5a019feb4..c1c45525a 100644 --- a/app/views/mod_warning/log.html.erb +++ b/app/views/mod_warning/log.html.erb @@ -1,42 +1,49 @@ -

Warnings sent to <%= user_link @user %>

+<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> - - - - - - - - - <% @warnings.each do |w| %> - - - - - - - - <% end %> -
DateTypeFromExcerptStatus
- <%= time_ago_in_words(w.created_at) %> ago - - <% if w.suspension? %> - <% diff = ((w.suspension_end - w.created_at) / (3600 * 24)).to_i %> - Suspension (<%= diff %>d) - <% else %> - Warning - <% end %> - <%= user_link w.author %><%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> - <% if w.suspension_active? %> - Current - <%= form_tag lift_mod_warning_url(@user.id), method: :post do %> - <%= submit_tag '(lift)', class: 'link is-red' %> - <% end %> - <% elsif w.active %> - Unread - <% elsif w.read %> - Read - <% else %> - Lifted - <% end %> -
+<%= render 'users/tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Previously Sent Warnings

+ + <% if @warnings.size == 0 %> +

No warnings found for this user.

+ <% end %> + + <% @warnings.each do |w| %> +
+
+
+ <% if w.suspension_active? %> + <%= form_tag lift_mod_warning_url(@user.id), method: :post, class: 'mod-warnings-clear-form' do %> + <%= submit_tag '(lift)', class: 'link is-red' %> + <% end %> · + Current + <% elsif w.active %> + Unread + <% elsif w.read %> + Read + <% else %> + Lifted + <% end %> +
+
+ <% if w.is_suspension %> + <% diff = ((w.suspension_end - w.created_at) / (3600 * 24)).to_i %> + Suspension + length: <%= diff %>d + <% else %> + Warning + <% end %> +
+
<%= time_ago_in_words(w.created_at) %> ago by <%= user_link w.author %>
+
+
+ <%= raw(sanitize(render_markdown(w.body), scrubber: scrubber)) %> +
+
+ <% end %> +
+
diff --git a/app/views/mod_warning/new.html.erb b/app/views/mod_warning/new.html.erb index 63f91ca38..d45550f27 100644 --- a/app/views/mod_warning/new.html.erb +++ b/app/views/mod_warning/new.html.erb @@ -2,80 +2,94 @@ <%= render 'posts/markdown_script' %> <% end %> +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> + <%= render 'posts/image_upload' %> +<%= render 'users/tabs', user: @user %> -

Warn or suspend <%= user_link @user %>

+
+ <%= render 'shared/user_mod_sidebar', user: @user %> -
-

Use the warning tool only against users who have violated the site rules. Prefer other measurements, such as friendly asking the user to stop certain behaviors in a comment.

-
+
+

Warn or Suspend User

-<%= form_for @warning, url: create_mod_warning_path(@user.id), method: :post do |f| %> -
-
- 1. Choose a template +
+

Use the warning tool only against users who have violated the site rules. Prefer other measures where possible, such as a public + comment.

-
-
-

Choose a template, which explains, why you are contacting the user. If none is applicable, choose to send a custom message.

- - + + <%= form_for @warning, url: create_mod_warning_path(@user.id), method: :post do |f| %> +
+
+ 1. Choose a template
-
-
- 2. Review the message -
-
-
-

Review the generated message and add details. Do not add salutations or information about possible suspensions, as they are generated automatically.

+
+
+

Choose a template that explains why you are contacting the user. If none are applicable, choose to send a + custom message.

+ + +
+
+
+ 2. Review the message +
+
+
+

Review the generated message and add details. Do not add salutations or information about possible + suspensions, as they are generated automatically.

-
- <%= render 'shared/body_field', f: f, field_name: :body, field_label: 'Body' %> -
+
+ <%= render 'shared/body_field', f: f, field_name: :body, field_label: 'Body' %> +
+
-
-
- 3. Choose optional suspension -
-
-
-

Decide, whether or not to suspend the user, and if, for how long. Choose an optional message shown publicly on the user profile.

+
+ 3. Choose optional suspension +
+
+
+

Decide whether or not to suspend the user, and if so for how long. Choose an optional message shown + publicly on the user profile.

- <% if @prior_warning_count == 0 %> -

Info: This user has no prior warnings. The system recommends issuing only a warning, unless the user is destructive and needs to be stopped immediately.

- <% elsif @prior_warning_count >= 5 %> -

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for 365 days (the maximum).

- <% else %> - <% lengths = { 1 => 3, 2 => 7, 3 => 30, 4 => 180 } %> -

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for <%= lengths[@prior_warning_count] %> days.

- <% end %> + <% if @prior_warning_count == 0 %> +

Info: This user has no prior warnings. The system recommends issuing only a warning, unless the user is destructive and needs to be stopped immediately.

+ <% elsif @prior_warning_count >= 5 %> +

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for 365 days (the maximum).

+ <% else %> + <% lengths = { 1 => 3, 2 => 7, 3 => 30, 4 => 180 } %> +

Info: This user has <%= @prior_warning_count %> prior warnings. The system recommends suspending them for <%= lengths[@prior_warning_count] %> days.

+ <% end %> -
- <%= f.label :is_suspension, 'Suspend this user account?', class: 'form-element' %> - - -
+
+ <%= f.label :is_suspension, 'Suspend this user account?', class: 'form-element' %> + + +
-
- <%= f.label :suspension_duration, 'If suspending, for how long?', class: 'form-element' %> -
Enter the number of days. At least 1, at most 365.
- <%= f.number_field :suspension_duration, in: 1..365, class: 'form-element' %> -
+
+ <%= f.label :suspension_duration, 'If suspending, for how long?', class: 'form-element' %> +
Enter the number of days. At least 1, at most 365.
+ <%= f.number_field :suspension_duration, in: 1..365, class: 'form-element' %> +
-
- <%= f.label :suspension_public_notice, 'If suspending, what public notice, if any, do you want to show?', class: 'form-element' %> - <%= f.select :suspension_public_notice, options_for_select([['for rule violations', 'for rule violations'], ['to cool down', 'to cool down']]), { include_blank: true }, class: 'form-element' %> +
+ <%= f.label :suspension_public_notice, 'If suspending, what public notice, if any, do you want to show?', class: 'form-element' %> + <%= f.select :suspension_public_notice, options_for_select([['for rule violations', 'for rule violations'], ['to cool down', 'to cool down']]), { include_blank: true }, class: 'form-element' %> +
+
- + <% end %> + +
-<% end %> diff --git a/app/views/moderator/user_vote_summary.html.erb b/app/views/moderator/user_vote_summary.html.erb index 65fb666a3..9c2d3e7b1 100644 --- a/app/views/moderator/user_vote_summary.html.erb +++ b/app/views/moderator/user_vote_summary.html.erb @@ -1,53 +1,62 @@ -

Vote Summary: <%= user_link @user %>>

-

- This is a summary of votes cast and received by this user. This may help you to identify voting patterns and - sock puppets, but use caution: what you see as a pattern may also be coincidence. Look for conclusive undeniable - patterns before using this data for sanctions. -

+<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -

- Key: - <%= text_bg 'yellow-200', '> 20%', class: 'has-padding-1 has-margin-horizontal-1' %> - <%= text_bg 'yellow-700', '> 30%', class: 'has-padding-1 has-margin-horizontal-1' %> - <%= text_bg 'red-200', '> 40%', class: 'has-padding-1 has-margin-horizontal-1' %> - <%= text_bg 'red-700', '> 50%', class: 'has-color-white has-padding-1 has-margin-horizontal-1' %> -

+<%= render 'users/tabs', user: @user %> -<% [:cast, :received].each do |type| %> -

Votes <%= type %>

+
+ <%= render 'shared/user_mod_sidebar', user: @user %> - - - - - - - - - - - <% @vote_data[type].breakdown.each do |key, count| %> - - - - - <% pct = count * 100.0 / @vote_data[type].total %> - - - <% end %> - -
<%= type == :cast ? 'To' : 'From' %> userVote typeVote count% of total
<%= user_link @users.select { |x| x.id == key[0] }[0] %><%= key[1] %><%= count %> - <% if pct >= 50 %> - <%= text_bg 'red-700', number_to_percentage(pct, precision: 2), class: 'has-color-white has-padding-1' %> - <% elsif pct >= 40 %> - <%= text_bg 'red-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% elsif pct >= 30 %> - <%= text_bg 'yellow-700', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% elsif pct >= 20 %> - <%= text_bg 'yellow-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> - <% else %> - <%= number_to_percentage(pct, precision: 2) %> - <% end %> -
-<% end %> +
+

Vote Summary

+

+ This is a summary of votes cast and received by this user. This may help you to identify voting patterns and + sock puppets, but use caution: what you see as a pattern may also be coincidence. Look for conclusive undeniable + patterns before using this data for sanctions. +

+ +

+ Key: + <%= text_bg 'yellow-200', '> 20%', class: 'has-padding-1 has-margin-horizontal-1' %> + <%= text_bg 'yellow-700', '> 30%', class: 'has-padding-1 has-margin-horizontal-1' %> + <%= text_bg 'red-200', '> 40%', class: 'has-padding-1 has-margin-horizontal-1' %> + <%= text_bg 'red-700', '> 50%', class: 'has-color-white has-padding-1 has-margin-horizontal-1' %> +

+ <% [:cast, :received].each do |type| %> +

Votes <%= type %>

+ + + + + + + + + + + + <% @vote_data[type].breakdown.each do |key, count| %> + + + + + <% pct = count * 100.0 / @vote_data[type].total %> + + + <% end %> + +
<%= type == :cast ? 'To' : 'From' %> userVote typeVote count% of total
<%= user_link @users.select { |x| x.id == key[0] }[0] %><%= key[1] %><%= count %> + <% if pct >= 50 %> + <%= text_bg 'red-700', number_to_percentage(pct, precision: 2), class: 'has-color-white has-padding-1' %> + <% elsif pct >= 40 %> + <%= text_bg 'red-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% elsif pct >= 30 %> + <%= text_bg 'yellow-700', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% elsif pct >= 20 %> + <%= text_bg 'yellow-200', number_to_percentage(pct, precision: 2), class: 'has-padding-1' %> + <% else %> + <%= number_to_percentage(pct, precision: 2) %> + <% end %> +
+ <% end %> +
+
diff --git a/app/views/shared/_user_mod_sidebar.html.erb b/app/views/shared/_user_mod_sidebar.html.erb new file mode 100644 index 000000000..6c890e4c8 --- /dev/null +++ b/app/views/shared/_user_mod_sidebar.html.erb @@ -0,0 +1,88 @@ +
+
+
+ User Moderation Tools +
+
+
+ <%= render 'users/common_card', user: user, ckb: false %> +
+
+
+ +
+
+
diff --git a/app/views/users/_tabs.html.erb b/app/views/users/_tabs.html.erb index fb2c9d191..8047eae2b 100644 --- a/app/views/users/_tabs.html.erb +++ b/app/views/users/_tabs.html.erb @@ -28,4 +28,22 @@ <%= link_to network_path(user), class: "tabs--item #{current_page?(network_path(user)) ? 'is-active' : ''}" do %> All Communities <% end %> +
+ <% if current_user&.moderator? %> + <%= link_to mod_user_path(user), class: "tabs--item #{( + current_page?(mod_user_path(user)) || + current_page?(full_user_log_path(user)) || + current_page?(user_annotations_path(user)) || + current_page?(mod_vote_summary_path(user)) || + current_page?(user_privileges_path(user)) || + current_page?(mod_warning_log_path(user)) || + current_page?(new_mod_warning_path(user)) || + current_page?(mod_delete_path(user)) || + current_page?(mod_delete_network_account_path(user)) || + current_page?(mod_failban_path(user)) || + current_page?(start_impersonating_path(user)) + ) ? 'is-active' : ''}" do %> + Moderator Tools <% if @user&.community_user&.mod_warnings&.size&.positive? %> (<%= pluralize(@user.community_user.mod_warnings.count, 'message') %>) <% end %> + <% end %> + <% end %>
diff --git a/app/views/users/annotations.html.erb b/app/views/users/annotations.html.erb index 858a5fdab..26c5b784a 100644 --- a/app/views/users/annotations.html.erb +++ b/app/views/users/annotations.html.erb @@ -1,35 +1,39 @@ -<% - title = 'User annotations' -%> +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -<% content_for :title, title %> +<%= render 'tabs', user: @user %> -

<%= title %>

+
+ <%= render 'shared/user_mod_sidebar', user: @user %> -
- Add an annotation - <% if defined?(@log) && @log.errors.any? %> -
- There was an error while trying to save your annotation. -
    - <% @log.errors.full_messages.each do |msg| %> -
  • <%= msg %>
  • - <% end %> -
-
- <% end %> - <%= form_tag annotate_user_path(@user), method: :post do %> -
- <%= label_tag :comment, 'Comment', class: 'form-element' %> - <%= text_field_tag :comment, params[:comment], class: 'form-element' %> -
+
+

User annotations

- <%= submit_tag 'Save', class: 'button is-filled' %> - <% end %> -
+
+ Add an annotation + <% if defined?(@log) && @log.errors.any? %> +
+ There was an error while trying to save your annotation. +
    + <% @log.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + <%= form_tag annotate_user_path(@user), method: :post do %> +
+ <%= label_tag :comment, 'Comment', class: 'form-element' %> + <%= text_field_tag :comment, params[:comment], class: 'form-element' %> +
-
-

<%= pluralize(@logs.count, 'log') %>

-
+ <%= submit_tag 'Save', class: 'button is-filled' %> + <% end %> +
-<%= render 'admin/log_table' %> +
+

<%= pluralize(@logs.count, 'log') %>

+
+ + <%= render 'admin/log_table' %> +
+
diff --git a/app/views/users/full_log.html.erb b/app/views/users/full_log.html.erb index bbd01e835..7c1f8cde8 100644 --- a/app/views/users/full_log.html.erb +++ b/app/views/users/full_log.html.erb @@ -1,44 +1,52 @@ <% content_for :title, "Full Activity Log: #{rtl_safe_username(@user)}" %> -

Full activity log for <%= user_link @user %>

+<%= render 'tabs', user: @user %> -

This is a filterable log for all activity by the user. You can consult it for moderation decisions. Do not share this information to people, who do not have access to it.

+
+ <%= render 'shared/user_mod_sidebar', user: @user %> -<% if params[:filter] == 'interesting' %> -

You are looking at negative interactions the user had with this site. These are not necessarily bad, just actions at which you should look more closely. This list includes deleted comments, rejected flags and edit suggestions and negatively received posts.

-<% end %> +
+

Full activity log

-
- - Show all events - - <% if @interesting > 0 %> - - Negative - <%= @interesting %> - +

This is a filterable log for all activity by the user. You can consult it for moderation decisions. Do not share this information to people, who do not have access to it.

+ + <% if params[:filter] == 'interesting' %> +

You are looking at negative interactions the user had with this site. These are not necessarily bad, just actions at which you should look more closely. This list includes deleted comments, rejected flags and edit suggestions and negatively received posts.

<% end %> - + + + +
-<%= render 'activity_items', mod: true %> + <%= render 'activity_items', mod: true %> +
+
diff --git a/app/views/users/mod.html.erb b/app/views/users/mod.html.erb index 647bd7e1e..18562929c 100644 --- a/app/views/users/mod.html.erb +++ b/app/views/users/mod.html.erb @@ -1,39 +1,198 @@ <% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -

Moderator Tools: <%= user_link @user %>

+<%= render 'tabs', user: @user %> -
-
Links
-
-
    -
  • full activity log
  • -
  • <%= link_to 'annotations on user', user_annotations_path(@user) %>
  • -
  • privileges
  • -
  • warnings and suspensions sent to user <% if @user.community_user.suspended? %>(includes lifting the suspension)<% end %>
  • -
  • warn or suspend user
  • -
  • <%= link_to 'vote summary', mod_vote_summary_path(@user) %>
  • - <% if current_user.developer %> -
  • <%= link_to 'impersonate', start_impersonating_path(@user), class: 'is-yellow' %>
  • - <% end %> -
-
-
+
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Dashboard

+

Please note that information shown in these user moderation tools is sensitive and should not be shared with anyone outside the moderator and admin team.

+ +
+
Account Information
+
+ + + + + + + + + + + + + +
User Name<%= rtl_safe_username(@user) %>
Account ID#<%= @user.id %>
Joined<%= @user.created_at.strftime("%Y-%m-%d") %> (network), <%= @user.community_user.created_at.strftime("%Y-%m-%d") %> (community)
+
+
+ +
+
Activity Summary
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BehaviorSince CreationLast YearLast MonthMost Recent
Posts Written + <%= @user.posts.count %> + + <%= @user.posts.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.posts.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_post = @user.posts.last %> + <% if last_post %> + <%= time_ago_in_words(last_post.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Votes Cast + <%= @user.votes.count %> + + <%= @user.votes.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.votes.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_vote = @user.votes.last %> + <% if last_vote %> + <%= time_ago_in_words(last_vote.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Comments written + <%= @user.comments.count %> + + <%= @user.comments.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.comments.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_comment = @user.comments.last %> + <% if last_comment %> + <%= time_ago_in_words(last_comment.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Edits Suggested + <%= @user.suggested_edits.count %> + + <%= @user.suggested_edits.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.suggested_edits.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_suggested_edit = @user.suggested_edits.last %> + <% if last_suggested_edit %> + <%= time_ago_in_words(last_suggested_edit.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
Flags Raised + <%= @user.flags.count %> + + <%= @user.flags.where(created_at: 360.days.ago..DateTime.now).count %> + + <%= @user.votes.where(created_at: 30.days.ago..DateTime.now).count %> + + <% last_flag = @user.flags.last %> + <% if last_flag %> + <%= time_ago_in_words(last_flag.created_at, locale: :en_abbrev) %> ago + <% else %> + never + <% end %> +
+
+
-
-
Danger Zone
-
-

Take care! Actions in this section may not be reversible, and you will not be asked to confirm - after initiating an action.

-
- <%= link_to 'Delete community profile', soft_delete_user_path(@user.id, type: 'profile'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled', role: 'button' %> - <% if current_user.is_global_moderator || current_user.is_global_admin %> - <%= link_to 'Delete user network-wide', soft_delete_user_path(@user.id, type: 'user'), remote: true, - method: :delete, class: 'js-soft-delete button is-danger is-filled', role: 'button' %> - <% end %> - <% if current_user.is_global_admin %> - <%= link_to 'Feed to STAT (180 days)', hellban_user_path(@user), method: :post, class: 'button is-danger is-filled', role: 'button' %> - <% end %> +
+
Moderation Summary
+
+ <% annotations_count = AuditLog.where(log_type: 'user_annotation', related: @user).count %> + <% warnings_count = ModWarning.where(community_user: @user.community_user, is_suspension: false).count %> + <% suspensions_count = ModWarning.where(community_user: @user.community_user, is_suspension: true).count %> + + + + + + + + + + + + + + + + + +
Annotations + <% if annotations_count > 0 %> + <%= annotations_count %> + <% else %> + 0 + <% end %> +
Currently Suspended? + <% if @user.community_user.suspended? %> + yes + <% else %> + no + <% end %> +
Warnings + <% if warnings_count > 0 %> + <%= warnings_count %> + <% else %> + 0 + <% end %> +
Suspensions + <% if suspensions_count > 0 %> + <%= suspensions_count %> + <% else %> + 0 + <% end %> +
+
diff --git a/app/views/users/mod_delete.html.erb b/app/views/users/mod_delete.html.erb new file mode 100644 index 000000000..518f10a0b --- /dev/null +++ b/app/views/users/mod_delete.html.erb @@ -0,0 +1,27 @@ +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> + +<%= render 'tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Delete Account

+ +

Some users are just blatant spammers or trolls and some users are just unwilling to follow site rules, even after repeated warnings and suspensions. As a moderator, you may delete the user account in these cases.

+ +
+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+
+ +
+
+

Delete Community Profile

+

Delete the community profile of users who are unwilling to follow the rules of this site, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on one site, once you have confirmed their identity and request.

+ + <%= link_to 'Delete community profile', soft_delete_user_path(@user.id, type: 'profile'), remote: true, + method: :delete, class: 'js-soft-delete button is-danger is-filled' %> +
+
+
+
diff --git a/app/views/users/mod_delete_network_account.html.erb b/app/views/users/mod_delete_network_account.html.erb new file mode 100644 index 000000000..6df3c2409 --- /dev/null +++ b/app/views/users/mod_delete_network_account.html.erb @@ -0,0 +1,27 @@ +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> + +<%= render 'tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Network-wide Account Deletion

+ +

As a global moderator, you may delete the user account network-wide.

+ +
+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+
+ +
+
+

Delete User Network-wide

+

Delete the account network-wide for users who are unwilling to follow the rules of this network, even after repeated warnings and suspensions. Choose this option if a user has requested deletion of their profile on all sites, once you have confirmed their identity and request.

+ + <%= link_to 'Delete user network-wide', soft_delete_user_path(@user.id, type: 'user'), remote: true, + method: :delete, class: 'js-soft-delete button is-danger is-filled' %> +
+
+
+
diff --git a/app/views/users/mod_failban.html.erb b/app/views/users/mod_failban.html.erb new file mode 100644 index 000000000..b7a2b95ce --- /dev/null +++ b/app/views/users/mod_failban.html.erb @@ -0,0 +1,26 @@ +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> + +<%= render 'tabs', user: @user %> + +
+ <%= render 'shared/user_mod_sidebar', user: @user %> + +
+

Fail-ban

+ +
+

Take care! These actions may not be reversible and you will not be asked to confirm after initiating an action.

+
+ +
+ <% if current_user.is_global_admin %> +
+

Feed to STAT

+

Blatant spammers and trolls may be fed into STAT ("Stop The Awful Troll") which causes the system to fail-ban them and limits the amount of damage they might possibly do. Only use for accounts where you are sure that they are never going to constructively use this site.

+ + <%= link_to 'Feed to STAT (180 days)', failban_user_path(@user), method: :post, class: 'button is-danger is-filled' %> +
+ <% end %> +
+
+
diff --git a/app/views/users/mod_privileges.html.erb b/app/views/users/mod_privileges.html.erb index ed62efb6d..0f0428ea3 100644 --- a/app/views/users/mod_privileges.html.erb +++ b/app/views/users/mod_privileges.html.erb @@ -1,176 +1,185 @@ -<% content_for :title, "Moderator Tools: #{@user.username}" %> +<% content_for :title, "Moderator Tools: #{rtl_safe_username(@user)}" %> -

Privileges of <%= link_to @user.username, user_path(@user) %>

+<%= render 'tabs', user: @user %> -
-
- ability - page - Abilities -
- <% @abilities.each do |a| %> - <% next if a.internal_id == 'mod' %> - <% ua = @user.privilege a.internal_id %> -
-
-
- -
-
-

- <%= a.name %> -

-

<%= a.summary %>

- <% unless ua.nil? %> -

- Delete -

- <% end %> -
-
- <% if ua.nil? %> - - <% elsif ua.suspended? %> - - <% else %> - -
-
suspend ability to <%= a.name %>
- -
in days; leave blank for permanent
- +
+ <%= render 'shared/user_mod_sidebar', user: @user %> - -
will be privately shown to user
- +
+

User Privileges

- +
+
+ ability + page + Abilities +
+ <% @abilities.each do |a| %> + <% next if a.internal_id == 'mod' %> + <% ua = @user.privilege a.internal_id %> +
+
+
+ +
+
+

+ <%= a.name %> +

+

<%= a.summary %>

+ <% unless ua.nil? %> +

+ Delete +

+ <% end %>
- <% end %> +
+ <% if ua.nil? %> + + <% elsif ua.suspended? %> + + <% else %> + +
+
suspend ability to <%= a.name %>
+ +
in days; leave blank for permanent
+ + + +
will be privately shown to user
+ + + +
+ <% end %> +
+
+ <% end %> +
- <% end %> - -
-<% if current_user.admin? %> -
-
- Roles -
-
-
-
- + <% if current_user.admin? %> +
+
+ Roles
-
-

- Moderator -

-

Moderators can unilaterally close and delete posts, can feature and lock posts - and may impose restrictions on user accounts.

+
+
+
+ +
+
+

+ Moderator +

+

Moderators can unilaterally close and delete posts, can feature and lock posts + and may impose restrictions on user accounts.

+
+
+ <% if @user.moderator? %> + + <% else %> + + <% end %> +
+
-
- <% if @user.community_user&.is_moderator %> - - <% else %> - - <% end %> + <% end %> + <% if current_user.is_global_admin %> +
+
+
+ +
+
+

+ Administrator +

+

Administrators can edit site settings and user roles.

+
+
+ <% if @user.is_global_moderator %> + + <% else %> + + <% end %> +
-
-<% end %> -<% if current_user.global_admin? %> -
-
-
- -
-
-

- Administrator -

-

Administrators can edit site settings and user roles.

-
-
- <% if @user.community_user&.is_admin %> - - <% else %> - - <% end %> -
-
-
-
-
-
- -
-
-

- Network-wide Moderator -

-

This user will have moderator status on every site in this network.

-
-
- <% if @user.global_moderator? %> - - <% else %> - - <% end %> -
-
-
-
-
-
- -
-
-

- Network-wide Admin -

-

This user will have admin status on every site in this network.

-
-
- <% if @user.global_admin? %> - <% if @user.id == current_user.id %> - - <% else %> - - <% end %> - <% else %> - - <% end %> -
-
-
-<% end %> -<% if current_user.global_admin? && current_user.staff? %> -
-
-
- +
+
+
+ +
+
+

+ Network-wide Moderator +

+

This user will have moderator status on every site in this network.

+
+
+ <% if @user.is_global_moderator %> + + <% else %> + + <% end %> +
+
-
-

- Staff -

-

The staff role doesn't carry any privileges, but designates the staff running this - site.

+
+
+
+ +
+
+

+ Network-wide Admin +

+

This user will have admin status on every site in this network.

+
+
+ <% if @user.is_global_admin %> + <% if @user.id == current_user.id %> + + <% else %> + + <% end %> + <% else %> + + <% end %> +
+
-
- <% if @user.staff? %> - - <% else %> - - <% end %> + <% end %> + <% if current_user.is_global_admin && current_user.staff? %> +
+
+
+ +
+
+

+ Staff +

+

The staff role doesn't carry any privileges, but designates the staff running this + site.

+
+
+ <% if @user.staff? %> + + <% else %> + + <% end %> +
+
+ <% end %>
+
-<% end %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 74a8ae1b4..627f663bb 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -64,27 +64,6 @@ Subscribe to user <% end %> <% end %> - <% if current_user&.at_least_moderator? %> - Moderator Tools <% if @user.community_user.mod_warnings&.size.positive? %> (<%= pluralize(@user.community_user.mod_warnings.count, 'message') %>) <% end %> - - <% end %> <% if current_user&.same_as?(@user) %> <%= link_to qr_login_code_path, class: 'button is-outlined is-small' do %> Mobile Sign In @@ -117,7 +96,7 @@
<% if @user.staff? %>
diff --git a/config/routes.rb b/config/routes.rb index fc4c5ca46..276fe870a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -224,7 +224,10 @@ get '/:id/mod/annotations', to: 'users#annotations', as: :user_annotations post '/:id/mod/annotations', to: 'users#annotate', as: :annotate_user get '/:id/mod/activity-log', to: 'users#full_log', as: :full_user_log - post '/:id/hellban', to: 'admin#hellban', as: :hellban_user + post '/:id/failban', to: 'admin#failban', as: :failban_user + get '/:id/mod/failban', to: 'users#mod_failban', as: :mod_failban + get '/:id/mod/delete', to: 'users#mod_delete', as: :mod_delete + get '/:id/mod/delete-network-account', to: 'users#mod_delete_network_account', as: :mod_delete_network_account get '/:id/avatar/:size', to: 'users#avatar', as: :user_auto_avatar end diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index ba2dcbfd4..7ea0f47f2 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -196,11 +196,11 @@ class AdminControllerTest < ActionController::TestCase end end - test 'hellban should correctly block the user' do + test 'failban should correctly block the user' do sign_in users(:global_admin) user = users(:standard_user) - try_hellban_user(user) + try_failban_user(user) user.reload assert_response(:found) @@ -223,8 +223,8 @@ def try_audit_logs(**params) get :audit_logs, params: params end - def try_hellban_user(user) - post :hellban, params: { id: user.id } + def try_failban_user(user) + post :failban, params: { id: user.id } end def try_impersonate_user(user)