Skip to content

Commit e5a279e

Browse files
authored
Merge pull request #2588 from mroderick/feature/admin-group-members-table
admin: improve group members page with pagination and search
2 parents 98df674 + 3f002fc commit e5a279e

5 files changed

Lines changed: 109 additions & 4 deletions

File tree

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ AllCops:
99
- bin/**/*
1010
- vendor/bundle/**/*
1111

12+
Lint/Syntax:
13+
Exclude:
14+
- app/views/**/*.haml
15+
1216
plugins:
1317
- rubocop-capybara
1418
- rubocop-performance

app/controllers/admin/groups_controller.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ def create
2222
def show
2323
@group = Group.find(params[:id])
2424
authorize @group
25+
26+
@eligible_count = @group.eligible_members.count
27+
@total_count = @group.members.count
28+
@pagy, @members = pagy(Group.members_by_recent_rsvp(@group), items: 20)
2529
end
2630

2731
private
2832

2933
def group_params
30-
params.expect(group: [:name, :description, :chapter_id])
34+
params.expect(group: %i[name description chapter_id])
3135
end
3236
end

app/models/group.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ class Group < ApplicationRecord
1111
scope :students, -> { where(name: 'Students') }
1212
scope :coaches, -> { where(name: 'Coaches') }
1313

14+
def self.members_by_recent_rsvp(group)
15+
group.members
16+
.joins('LEFT JOIN workshop_invitations ON workshop_invitations.member_id = members.id')
17+
.joins('LEFT JOIN workshops ON workshops.id = workshop_invitations.workshop_id')
18+
.select('members.*, MAX(workshops.date_and_time) as last_rsvp_at')
19+
.group('members.id')
20+
.order('MAX(workshops.date_and_time) DESC NULLS LAST')
21+
end
22+
1423
validates :name, presence: true, inclusion: { in: NAMES, message: 'Invalid name for Group' }
1524

1625
alias city chapter
Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,84 @@
1+
- content_for :head do
2+
%link{ href: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/css/tom-select.bootstrap5.min.css', rel: 'stylesheet', type: 'text/css' }
3+
%script{ src: 'https://cdn.jsdelivr.net/npm/tom-select@2.4.3/dist/js/tom-select.complete.min.js' }
4+
15
.container.py-4.py-lg-5
26
.row.mb-4
37
.col
48
%h1
59
= @group.name
6-
%small.text-muted #{@group.chapter.name}
10+
%small.text-muted= @group.chapter.name
11+
12+
.row
13+
.col
14+
%h3.mb-3 Members (#{@eligible_count} eligible, #{@total_count} total)
15+
16+
.row.mb-4
17+
.col-12.col-md-6
18+
= select_tag 'member_lookup_id', nil, class: 'form-control', placeholder: 'Search members by name or email...'
19+
.col-auto
20+
= link_to 'View Profile', '#', { class: 'btn btn-primary', id: 'view_profile' }
721

822
.row
923
.col
10-
%h3.mb-3 Members (#{@group.eligible_members.count} eligible, #{@group.members.count} total)
1124
%table.table.table-striped.table-hover
25+
%thead
26+
%tr
27+
%th Avatar
28+
%th Name
29+
%th Email
30+
%th Mobile
31+
%th T&C
1232
%tbody
13-
- @group.members.each do |member|
33+
- @members.each do |member|
1434
%tr
1535
%td= image_tag(member.avatar(32), class: 'rounded-circle', title: member.full_name, alt: member.full_name)
1636
%td= link_to member.full_name, admin_member_path(member)
1737
%td= mail_to member.email, member.email
1838
%td= member.mobile
39+
%td
40+
- if member.accepted_toc_at
41+
%i.fa.fa-check.text-success{ title: "Accepted on #{member.accepted_toc_at.to_date}" }
42+
- else
43+
%i.fa.fa-times.text-danger{ title: 'Not accepted' }
44+
45+
.row
46+
.col
47+
= render partial: 'shared/pagination', locals: { pagy: @pagy, model: 'member' }
48+
49+
-# TomSelect initialization
50+
:javascript
51+
document.addEventListener('DOMContentLoaded', function() {
52+
var control = document.getElementById('member_lookup_id');
53+
var viewLink = document.getElementById('view_profile');
54+
55+
var ts = new TomSelect(control, {
56+
create: false,
57+
maxItems: 1,
58+
placeholder: 'Search members by name or email...',
59+
valueField: 'id',
60+
labelField: 'full_name',
61+
searchField: ['full_name', 'email'],
62+
load: function(query, callback) {
63+
if (query.length < 3) {
64+
callback();
65+
return;
66+
}
67+
68+
var url = '/admin/members/search?q=' + encodeURIComponent(query);
69+
fetch(url)
70+
.then(function(response) { return response.json(); })
71+
.then(function(data) { callback(data); })
72+
.catch(function() { callback(); });
73+
},
74+
onChange: function(value) {
75+
if (value) {
76+
viewLink.href = '/admin/members/' + value;
77+
viewLink.classList.remove('disabled');
78+
} else {
79+
viewLink.href = '#';
80+
viewLink.classList.add('disabled');
81+
}
82+
}
83+
});
84+
});

spec/models/group_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,26 @@
2727
expect(group.eligible_members).to be_empty
2828
end
2929
end
30+
31+
describe '.members_by_recent_rsvp' do
32+
let(:group) { Fabricate(:group, name: 'Students') }
33+
let(:chapter) { group.chapter }
34+
35+
it 'orders members by most recent workshop RSVP' do
36+
old_workshop = Fabricate(:workshop, chapter: chapter, date_and_time: 1.month.ago)
37+
new_workshop = Fabricate(:workshop, chapter: chapter, date_and_time: 1.week.ago)
38+
39+
member_old = Fabricate(:member, groups: [group])
40+
member_new = Fabricate(:member, groups: [group])
41+
_member_no_rsvp = Fabricate(:member, groups: [group])
42+
43+
Fabricate(:workshop_invitation, workshop: old_workshop, member: member_old, attending: true)
44+
Fabricate(:workshop_invitation, workshop: new_workshop, member: member_new, attending: true)
45+
46+
results = Group.members_by_recent_rsvp(group).to_a
47+
48+
expect(results.first).to eq(member_new)
49+
expect(results.last).to eq(_member_no_rsvp)
50+
end
51+
end
3052
end

0 commit comments

Comments
 (0)