mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-10-31 19:29:26 -04:00
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:
4 files changed
+248
-24
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user