Filter deleted members by default and enable bulk ban on admin members (#5060)

* Filter deleted members by default and enable bulk ban on admin members

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Drop instagram

* Default sort

* Update specs

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-24 14:30:50 +10:30
1 parent e0b24f860b
commit d491df87fc
4 files changed
+248 -24

No files matched your search

+24 -2
View File
@@ -8,8 +8,10 @@ module Admin
responders :flash
def index
@members = Member.order(:login_name)
@members = @members.where("login_name ILIKE ?", "%#{search_term}%") if search_term.present?
@status = params[:status] == 'deactivated' ? 'deactivated' : 'active'
@members = @status == 'deactivated' ? Member.discarded : Member.kept
@members = @members.includes(:roles).order(created_at: :desc)
@members = @members.where("login_name ILIKE ? OR email ILIKE ?", "%#{search_term}%", "%#{search_term}%") if search_term.present?
@members = @members.paginate(page: params[:page])
end
@@ -27,6 +29,26 @@ module Admin
def destroy
@member = Member.find_by!(slug: params[:slug])
@member.discard
redirect_to admin_members_path(status: params[:status])
end
def restore
@member = Member.find_by!(slug: params[:slug])
@member.undiscard
redirect_to admin_members_path(status: 'deactivated')
end
def bulk_ban
member_ids = params[:member_ids]
if member_ids.present? && member_ids.is_a?(Array)
members_to_ban = Member.kept.where(id: member_ids)
# Prevent admins from self-banning via bulk ban
members_to_ban = members_to_ban.where.not(id: current_member.id)
members_to_ban.find_each(&:discard)
flash[:notice] = "Selected members have been banned."
else
flash[:alert] = "No members were selected."
end
redirect_to admin_members_path
end
+130 -21
View File
@@ -2,32 +2,141 @@
%li.breadcrumb-item= link_to 'Admin', admin_path
%li.breadcrumb-item.active= link_to 'Members', admin_members_path
.row
.row.mb-3
.col-md-6
= bootstrap_form_tag url: admin_members_path, method: :get, layout: :horizontal do |f|
= hidden_field_tag :status, @status
.input-group
%span.input-group-text= icon 'fas', 'user'
= f.text_field(:q, 'aria-label': 'search members', placeholder: 'search members', hide_label: true)
= f.text_field(:q, value: params[:q], 'aria-label': 'search members', placeholder: 'search members', hide_label: true)
= f.submit 'search', class: 'btn btn-secondary'
%table.table.table-striped
%thead
%tr
%th{scope: "col"} #
%th{scope: "col"} Login Name
- @members.each do |member|
%tr
%td= render 'members/tiny', member: member
%td= member.login_name
%td
%ul
- member.roles.pluck(:name).each do |r|
%li= r
%td
= link_to edit_admin_member_path(member), class: 'btn' do
= edit_icon
= link_to admin_member_path(member), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-light text-danger' do
= icon 'fas', 'ban'
Ban member
%ul.nav.nav-tabs.mb-3
%li.nav-item
= link_to 'Active', admin_members_path(status: 'active', q: params[:q]), class: "nav-link #{@status == 'active' ? 'active' : ''}"
%li.nav-item
= link_to 'Deactivated', admin_members_path(status: 'deactivated', q: params[:q]), class: "nav-link #{@status == 'deactivated' ? 'active' : ''}"
- if @status == 'active'
= form_tag bulk_ban_admin_members_path, method: :post, id: 'bulk_ban_form' do
.d-flex.justify-content-between.align-items-center.mb-2
%div
= submit_tag 'Bulk Ban Selected', class: 'btn btn-danger btn-sm', data: { confirm: 'Are you sure you want to ban the selected members?' }
.table-responsive
%table.table.table-striped.table-sm.align-middle
%thead
%tr
%th{scope: "col"}
%input#select_all_members{type: "checkbox", onclick: "document.querySelectorAll('.member-select-checkbox').forEach(cb => cb.checked = this.checked);"}
%th{scope: "col"} #
%th{scope: "col"} Login Name
%th{scope: "col"} Email
%th{scope: "col"} Confirmed
%th{scope: "col"} Last Login
%th{scope: "col"} Bio
%th{scope: "col"} Roles
%th{scope: "col"} Photos
%th{scope: "col"} Plantings
%th{scope: "col"} Forums
%th{scope: "col"} Gardens
%th{scope: "col"} Harvests
%th{scope: "col"} Jobs
%th{scope: "col"} Actions
%tbody
- @members.each do |member|
%tr
%td
- unless member.id == current_member.id
= check_box_tag "member_ids[]", member.id, false, class: 'member-select-checkbox'
%td= render 'members/tiny', member: member
%td= link_to member.login_name, member
%td= member.email
%td
- if member.confirmed?
%span.badge.bg-success Yes
- else
%span.badge.bg-secondary No
%td
- if member.last_sign_in_at.present?
%small= l(member.last_sign_in_at, format: :short)
- else
%small.text-muted Never
%td
- if member.bio.present?
%small{title: member.bio}= truncate(member.bio, length: 255)
%td
- if member.roles.any?
%small= member.roles.map(&:name).join(', ')
%td= member.photos_count || 0
%td= member.plantings_count || 0
%td= member.forums_count || 0
%td= member.gardens_count || 0
%td= member.harvests_count || 0
%td= member.jobs_count || 0
%td
.btn-group.btn-group-sm
= link_to edit_admin_member_path(member), class: 'btn btn-outline-secondary', title: 'Edit roles' do
= edit_icon
- unless member.id == current_member.id
= link_to admin_member_path(member, status: @status), method: :delete, data: { confirm: 'Are you sure you want to ban this member?' }, class: 'btn btn-outline-danger', title: 'Ban member' do
= icon 'fas', 'ban'
- else
.table-responsive
%table.table.table-striped.table-sm.align-middle
%thead
%tr
%th{scope: "col"} #
%th{scope: "col"} Login Name
%th{scope: "col"} Email
%th{scope: "col"} Confirmed
%th{scope: "col"} Last Login
%th{scope: "col"} Bio
%th{scope: "col"} Instagram
%th{scope: "col"} Roles
%th{scope: "col"} Photos
%th{scope: "col"} Plantings
%th{scope: "col"} Forums
%th{scope: "col"} Gardens
%th{scope: "col"} Harvests
%th{scope: "col"} Jobs
%th{scope: "col"} Actions
%tbody
- @members.each do |member|
%tr
%td= render 'members/tiny', member: member
%td= member.login_name
%td= member.email
%td
- if member.confirmed?
%span.badge.bg-success Yes
- else
%span.badge.bg-secondary No
%td
- if member.last_sign_in_at.present?
%small= l(member.last_sign_in_at, format: :short)
- else
%small.text-muted Never
%td
- if member.bio.present?
%small{title: member.bio}= truncate(member.bio, length: 255)
%td
- if member.instagram_handle.present?
%small= member.instagram_handle
%td
- if member.roles.any?
%small= member.roles.map(&:name).join(', ')
%td= member.photos_count || 0
%td= member.plantings_count || 0
%td= member.forums_count || 0
%td= member.gardens_count || 0
%td= member.harvests_count || 0
%td= member.jobs_count || 0
%td
= link_to restore_admin_member_path(member), method: :post, data: { confirm: 'Restore this member?' }, class: 'btn btn-sm btn-outline-success', title: 'Restore member' do
= icon 'fas', 'undo'
Restore
= page_entries_info @members
= will_paginate @members
+4 -1
View File
@@ -185,7 +185,10 @@ Rails.application.routes.draw do
end
namespace :admin do
resources :members, param: :slug
resources :members, param: :slug do
post :bulk_ban, on: :collection
post :restore, on: :member
end
resources :roles
resources :crops, param: :slug do
resources :crop_companions
+90
View File
@@ -0,0 +1,90 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin::Members', type: :request do
let(:admin_role) { create(:role, name: 'admin') }
let(:admin) { create(:member, roles: [admin_role]) }
let!(:active_member) do
create(
:member,
login_name: 'active_spammer',
email: 'active@example.com',
bio: 'A' * 300,
photos_count: 3,
plantings_count: 5,
forums_count: 1,
gardens_count: 2,
harvests_count: 4,
jobs_count: 6,
confirmed_at: Time.current,
last_sign_in_at: Time.current
)
end
let!(:deleted_member) do
member = create(:member, login_name: 'deleted_user', email: 'deleted@example.com')
member.discard
member
end
before do
login_as admin, scope: :member
end
describe 'GET /admin/members' do
context 'by default (Active tab)' do
it 'filters out deleted members and shows active members' do
get admin_members_path
expect(response).to have_http_status(:ok)
expect(response.body).to include('active_spammer')
expect(response.body).not_to include('deleted_user')
end
it 'surfaces login name, email, confirmed status, last login, truncated bio, and count attributes' do
get admin_members_path
expect(response.body).to include('active@example.com')
expect(response.body).to include('A' * 252 + '...') # truncated to 255 chars
expect(response.body).to include('Photos')
expect(response.body).to include('Plantings')
expect(response.body).to include('Forums')
expect(response.body).to include('Gardens')
expect(response.body).to include('Harvests')
expect(response.body).to include('Jobs')
end
end
context 'when requesting deactivated tab (status=deactivated)' do
it 'shows discarded members and excludes active members' do
get admin_members_path(status: 'deactivated')
expect(response).to have_http_status(:ok)
expect(response.body).to include('deleted_user')
expect(response.body).not_to include('active_spammer')
end
end
end
describe 'POST /admin/members/bulk_ban' do
it 'discards selected active members' do
expect do
post bulk_ban_admin_members_path, params: { member_ids: [active_member.id] }
end.to change { active_member.reload.discarded? }.from(false).to(true)
expect(response).to redirect_to(admin_members_path)
end
it 'does not allow admin to ban themselves via bulk ban' do
post bulk_ban_admin_members_path, params: { member_ids: [admin.id] }
expect(admin.reload.discarded?).to be false
end
end
describe 'POST /admin/members/:slug/restore' do
it 'undiscards a deactivated member' do
expect do
post restore_admin_member_path(deleted_member)
end.to change { deleted_member.reload.discarded? }.from(true).to(false)
expect(response).to redirect_to(admin_members_path(status: 'deactivated'))
end
end
end