diff --git a/app/controllers/admin/member_search_controller.rb b/app/controllers/admin/member_search_controller.rb new file mode 100644 index 000000000..7cc300ee2 --- /dev/null +++ b/app/controllers/admin/member_search_controller.rb @@ -0,0 +1,21 @@ +class Admin::MemberSearchController < Admin::ApplicationController + def index + member_params = params[:member_search] || {} + name = member_params[:name] + members = name.blank? ? Member.none : Member.find_members_by_name(name).select(:id, :name, :surname, :pronouns) + callback_url = member_params[:callback_url] || params[:callback_url] || results_admin_member_search_index_path + if members.size == 1 + query = { member_pick: { members: [members.first.id] } } + query_string = query.to_query + callback_url = "#{callback_url}?#{query_string}" + redirect_to callback_url and return + end + + render 'index', locals: { members: members, callback_url: callback_url } + end + + def results + members = Member.find(params[:member_pick][:members]) + render 'show', locals: { members: members } + end +end diff --git a/app/models/member.rb b/app/models/member.rb index 15ce7c427..59ef928aa 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -118,6 +118,11 @@ def recent_notes member_notes.where('created_at > ?', notes_from_date) end + def self.find_members_by_name(name) + name.strip! + name.eql?('') ? self.none : where("CONCAT(name, ' ', surname) ILIKE ?", "%#{name}%") + end + private def invitations_on(date) diff --git a/app/views/admin/member_search/_search_form.html.haml b/app/views/admin/member_search/_search_form.html.haml new file mode 100644 index 000000000..b1c33bc95 --- /dev/null +++ b/app/views/admin/member_search/_search_form.html.haml @@ -0,0 +1,6 @@ += simple_form_for :member_search, url: admin_member_search_index_path, method: :get, html: {multipart: true, novalidate: true } do |f| + .row + .col-12.col-md-6 + = f.input :name, label: 'Member Name', input_html: { placeholder: 'Enter member name' } + = f.button :submit, 'Search', class: 'btn btn-primary mt-3', name: nil + = f.hidden_field 'callback_url', value: callback_url diff --git a/app/views/admin/member_search/index.html.haml b/app/views/admin/member_search/index.html.haml new file mode 100644 index 000000000..0321bdad9 --- /dev/null +++ b/app/views/admin/member_search/index.html.haml @@ -0,0 +1,22 @@ +.container.py-4.py-lg-5 + .row.mb-4 + .col + %h1 Member search + + .row + .col.col-md-10.col-lg-8 + = render partial: 'search_form', locals: { callback_url: callback_url } + + .row + .col.col-md-10.col-lg-8 + - if members.present? + %h2 Select Member + = simple_form_for :member_pick, url: callback_url, method: :get do |f| + .row + .col-12.col-md-6 + = f.label :members, 'Member Name' + - members.each do |member| + .form-check + = f.check_box :members, { multiple: true, checked: member.id }, member.id, nil + = f.label "members_#{member.id}", member.name_and_surname, class: 'form-check-label' + = f.button :submit, 'Take me back', class: 'btn btn-primary mt-3', name: nil diff --git a/app/views/admin/member_search/show.html.haml b/app/views/admin/member_search/show.html.haml new file mode 100644 index 000000000..060a94ee9 --- /dev/null +++ b/app/views/admin/member_search/show.html.haml @@ -0,0 +1,12 @@ +.container.py-4.py-lg-5 + .row.mb-4 + .col + %h1 Results + + .row + .col.col-md-10.col-lg-8 + %h2 Search Results + %ul.list-group + - members.each do |member| + %li.list-group-item + = link_to member.name_and_surname, admin_member_path(member) diff --git a/config/routes.rb b/config/routes.rb index f5b29ccb0..f418fee2b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -140,6 +140,13 @@ end resources :testimonials, only: %i[index] + + resources :member_search, path: 'member-search', only: [:index, :results] do + collection do + get 'index' + get 'results' + end + end end get '/login', to: 'auth_services#new' diff --git a/docker-compose.yml b/docker-compose.yml index ac505c523..0ab4ea54a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,8 @@ services: db: image: postgres:17 + ports: + - 5433:5432 volumes: - db_data:/var/lib/postgresql/data environment: diff --git a/spec/controllers/admin/member_search_controller_spec.rb b/spec/controllers/admin/member_search_controller_spec.rb new file mode 100644 index 000000000..3184c743a --- /dev/null +++ b/spec/controllers/admin/member_search_controller_spec.rb @@ -0,0 +1,56 @@ +RSpec.describe Admin::MemberSearchController, type: :controller do + let(:member) {Fabricate.build(:member)} + + describe 'GET #index' do + context "when user is not logged in" do + before do + get :index + end + it "redirects to the home page" do + expect(response).to redirect_to(root_path) + end + end + + context "when user is an admin" do + let(:fake_relation) { instance_double('ActiveRecord::Relation') } + let(:fake_juliet) { instance_double('Member', id: 1, name: 'Juliet', surname: 'Montague') } + + before do + login_as_admin(member) + get :index + end + it "shows user the search page" do + expect(response).to have_http_status(:ok) + end + + context "and when admin user searches for a single existing user" do + before do + allow(Member).to receive(:find_members_by_name).with('Juliet').and_return(fake_relation) + allow(fake_relation).to receive(:select).with(any_args).and_return([fake_juliet]) + get :index, params: {member_search: {name: "Juliet", callback_url: root_path}} + end + it "redirects to the calling service" do + expect(response).to have_http_status(:found) + + uri = URI.parse(response.location) + redirect_params = Rack::Utils.parse_nested_query(uri.query) + + expect(redirect_params["member_pick"]["members"]).to eq(["1"]) + end + end + + context "and when an admin user searches and there are multiple results" do + let(:fake_romeo) { double('Member', id: 2, name: 'Romeo', surname: 'Capulet')} + + before do + allow(Member).to receive(:find_members_by_name).with('e').and_return(fake_relation) + allow(fake_relation).to receive(:select).with(any_args).and_return([fake_juliet, fake_romeo]) + get :index, params: {member_search: {name: 'e', callback_url: root_path}} + end + it "presents the found members on the index page" do + expect(response).to have_http_status(:ok) + end + end + end + end +end diff --git a/spec/features/admin/add_user_to_workshop_spec.rb b/spec/features/admin/add_user_to_workshop_spec.rb new file mode 100644 index 000000000..962c3e3b2 --- /dev/null +++ b/spec/features/admin/add_user_to_workshop_spec.rb @@ -0,0 +1,43 @@ +RSpec.describe 'Add a user to an existing workshop', js: true, type: :feature do + let(:member) {Fabricate(:member)} + + let!(:juliet) {Fabricate(:member, name: 'Juliet', surname: 'Capulet')} + let!(:romeo) { Fabricate(:member, name: 'Romeo', surname: 'Montague') } + let(:workshop) {Fabricate(:workshop)} + + before do + login_as_admin(member) + @start_page = "/admin/workshops/#{workshop.id}" + end + + scenario 'An admin searches and gets an exact match' do + visit @start_page + + params = {callback_url: @start_page.to_s}.to_query + visit "/admin/member-search?#{params}" + fill_in 'Member Name', with: juliet.name_and_surname + click_on 'Search' + expect(page).to have_current_path(@start_page, ignore_query: true) + end + + scenario 'An admin adds a member to a workshop' do + visit @start_page + + params = {callback_url: @start_page.to_s}.to_query + visit "/admin/member-search?#{params}" + fill_in 'Member Name', with: 'e' + click_on 'Search' + expect(page).to have_current_path('/admin/member-search/index', ignore_query: true) + + expect(page).to have_content('Romeo Montague') + expect(page).to have_unchecked_field('Romeo Montague') + check('Romeo Montague') + click_button'Take me back' + + expect(page).to have_current_path(@start_page, ignore_query: true) + uri = URI.parse(page.current_url) + params = Rack::Utils.parse_nested_query(uri.query).with_indifferent_access + + expect(params[:member_pick][:members]).to eq([romeo.id.to_s]) + end +end diff --git a/spec/features/admin/member_search_spec.rb b/spec/features/admin/member_search_spec.rb new file mode 100644 index 000000000..ed8649efb --- /dev/null +++ b/spec/features/admin/member_search_spec.rb @@ -0,0 +1,18 @@ +RSpec.feature 'admin member search', type: :feature do + scenario 'search returns single member to requesting service' do + Fabricate(:member, :name => 'Romeo', :surname => 'Montague') + Fabricate(:member, :name => 'Juliet', :surname => 'Capulet') + member = Fabricate(:member) + login_as_admin(member) + visit admin_member_search_index_path(callback_url: results_admin_member_search_index_path) + + fill_in 'member_search_name', with: 'Julie' + click_button 'Search' + + expect(page).to have_current_path(results_admin_member_search_index_path, ignore_query: true) + + expect(page).to have_content('Juliet Capulet') + end + + +end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 7517e0b17..7b7ab26ae 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -137,4 +137,33 @@ expect(member.flag_to_organisers?).to be true end end + + describe '#find_members' do + describe 'search by first name' do + it 'finds the member' do + expect(Member.find_members_by_name(member.name).first).to eq(member) + end + end + + describe 'search by last name' do + it 'finds the member' do + expect(Member.find_members_by_name(member.surname).first).to eq(member) + end + end + + describe 'search by full name' do + it 'finds the member' do + expect(Member.find_members_by_name("#{member.name} #{member.surname}").first).to eq(member) + end + end + + describe 'search bar is empty' do + it 'returns no members' do + Fabricate(:member) + expect(Member.all.size).to be > 0 + expect(Member.find_members_by_name('').size).to eq(0) + end + end + + end end