Skip to content

Commit 8c01979

Browse files
authored
Make users searchable (#121)
2 parents df54086 + af9eb9a commit 8c01979

File tree

10 files changed

+249
-5
lines changed

10 files changed

+249
-5
lines changed

app/controllers/users_controller.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ class UsersController < ApplicationController
33
before_action :set_user, only: %i[ edit update destroy ]
44

55
def index
6-
@users = User.all
6+
@users = User.all.search_with_params(user_search_params)
77
end
88

99
def new
@@ -52,4 +52,9 @@ def set_user
5252
def user_params
5353
params.expect(user: [ :email, :password, :is_admin ])
5454
end
55+
56+
def user_search_params
57+
return {} unless params[:search].present?
58+
params.expect(search: [ :email, :is_admin, :order ])
59+
end
5560
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Controller } from "@hotwired/stimulus";
2+
import { useDebounce } from "stimulus-use";
3+
4+
export default class extends Controller {
5+
static targets = ["search"];
6+
static debounces = ["submit"];
7+
8+
connect() {
9+
useDebounce(this, { wait: 300 });
10+
}
11+
12+
submit() {
13+
this.searchTarget.requestSubmit();
14+
}
15+
}

app/models/concerns/searcheable.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ def sort_order(order_from_params)
3333

3434
order_from_params
3535
end
36+
end
3637

37-
def search_with_params(params)
38+
included do
39+
scope :search_with_params, ->(params) do
3840
self
3941
.then { |scope| params[:state].present? ? scope.by_state(params[:state]) : scope }
4042
.then { |scope| params[:provider_id].present? ? scope.by_provider(params[:provider_id]) : scope }
4143
.then { |scope| params[:language_id].present? ? scope.by_language(params[:language_id]): scope }
4244
.then { |scope| params[:year].present? ? scope.by_year(params[:year]) : scope }
4345
.then { |scope| params[:month].present? ? scope.by_month(params[:month]) : scope }
4446
.then { |scope| params[:query].present? ? scope.search(params[:query]) : scope }
45-
.then { |scope| scope.order(created_at: sort_order(params[:order])) }
47+
.then { |scope| params[:order].present? ? scope.order(created_at: sort_order(params[:order].to_sym)) : scope }
4648
end
4749
end
4850
end

app/models/user.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ class User < ApplicationRecord
2424
validates :email, presence: true, uniqueness: true, format: URI::MailTo::EMAIL_REGEXP
2525
validates :password_digest, presence: true
2626

27+
scope :search_with_params, ->(params) do
28+
self
29+
.then { |scope| params[:email].present? ? scope.where("email ILIKE ?", "%#{params[:email]}%") : scope }
30+
.then { |scope| params[:is_admin].present? ? scope.where(is_admin: params[:is_admin]) : scope }
31+
.then { |scope| scope.order(created_at: params[:order]&.to_sym || :desc) }
32+
end
33+
2734
def topics
2835
Topic.where(provider_id: providers.pluck(:id))
2936
end

app/views/users/index.html.erb

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
<div class="page-heading">
22
<section class="section">
3+
<div class="card">
4+
<div class="card-header d-flex justify-content-between align-items-center">
5+
<h2 class="card-title">Search</h2>
6+
</div>
7+
<div class="card-content">
8+
<div class="card-body">
9+
<%= form_for :search, url: users_path, method: :get, data: { controller: "search", search_target: "search", turbo_frame: "user-list", turbo_action: "advance" } do |f| %>
10+
<div class="form-body">
11+
<div class="row">
12+
<div class="col-md-6 col-12">
13+
<div class="form-group">
14+
<%= f.label :email %>
15+
<%= f.text_field :email, value: params.dig(:search, :email), class: "form-control", data: { action: "input->search#submit" } %>
16+
</div>
17+
</div>
18+
<div class="col-md-6 col-12">
19+
<div class="form-group">
20+
<%= f.label "Role" %>
21+
<%= f.select :is_admin, options_for_select([["Admin", "true"], ["Contributor", "false"]], params.dig(:search, :is_admin)), { prompt: "Select user role" }, class: "form-select", data: { action: "change->search#submit" } %>
22+
</div>
23+
</div>
24+
<div class="col-md-6 col-12">
25+
<div class="form-group">
26+
<%= f.label :order %>
27+
<%= f.select :order, options_for_select([["By most recently added", :desc], ["By least recently added", :asc]], params.dig(:search, :order)), {}, class: "form-select", data: { action: "change->search#submit" } %>
28+
</div>
29+
</div>
30+
<div class="col-12 d-flex justify-content-end">
31+
<%= link_to "Clear", users_path, class: "btn btn-light-secondary me-1 mb-1" %>
32+
</div>
33+
</div>
34+
</div>
35+
<% end %>
36+
</div>
37+
</div>
38+
</div>
39+
340
<div class="card">
441
<div class="card-header d-flex justify-content-between align-items-center">
542
<h2>User List</h2>
@@ -20,7 +57,7 @@
2057
<% @users.each do |user| %>
2158
<tr>
2259
<td><%= user.email %></td>
23-
<td><%= user.is_admin %></td>
60+
<td><%= user.is_admin ? "✅" : "✖️" %></td>
2461
<td class="text-end">
2562
<%= link_to edit_user_path(user), class: "btn btn-secondary btn-sm pr-2" do %>
2663
<i class="bi bi-pencil"></i> Edit

spec/rails_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,6 @@
6868
config.filter_rails_from_backtrace!
6969
# arbitrary gems may also be filtered via:
7070
# config.filter_gems_from_backtrace("gem name")
71+
72+
config.include SystemHelpers, type: :system
7173
end

spec/support/capybara.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
Capybara::Selenium::Driver.new(app, browser: :chrome)
77
end
88

9+
Capybara.register_driver :selenium_chrome_headless do |app|
10+
options = Selenium::WebDriver::Chrome::Options.new(args: %w[headless disable-gpu window-size=1400,1400])
11+
12+
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
13+
end
14+
915
module CapybaraPage
1016
def page
1117
Capybara.string(response.body)
@@ -14,7 +20,7 @@ def page
1420

1521
RSpec.configure do |config|
1622
config.before(:each, type: :system) do
17-
driven_by :rack_test
23+
driven_by :selenium_chrome_headless
1824
end
1925

2026
config.before(:each, :debug, type: :system) do

spec/support/system_helpers.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module SystemHelpers
2+
def login_as(user)
3+
visit root_path
4+
click_link("Sign In")
5+
fill_in "email", with: user.email
6+
fill_in "password", with: user.password
7+
click_button("Sign in")
8+
end
9+
end

spec/system/topic_search_spec.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "Topics search", type: :system do
4+
let(:admin) { create(:user, :admin, email: "[email protected]") }
5+
let(:english) { create(:language, name: "English") }
6+
let(:spanish) { create(:language, name: "Spanish") }
7+
let!(:spanish_active_topic) { create(:topic, language: spanish, title: "Tratamiento del resfriado", created_at: Date.new(2025, 02, 03)) }
8+
let!(:english_active_topic) { create(:topic, language: english, title: "How to treat colds", description: "All the latest information about nasopharyngitis", created_at: Date.new(2025, 03, 04)) }
9+
let!(:english_archived_topic) { create(:topic, :archived, language: english, title: "Obsolete", created_at: Date.new(2023, 02, 01)) }
10+
11+
before do
12+
login_as(admin)
13+
click_link("Topics")
14+
end
15+
16+
it "shows all topics" do
17+
expect(page).to have_text(english_active_topic.title)
18+
expect(page).to have_text(spanish_active_topic.title)
19+
expect(page).to have_text(english_archived_topic.title)
20+
end
21+
22+
context "when searching by title" do
23+
it "only displays topics matching the search" do
24+
fill_in "search_query", with: "tratamiento"
25+
26+
expect(page).to have_text(spanish_active_topic.title)
27+
expect(page).not_to have_text(english_active_topic.title)
28+
expect(page).not_to have_text(english_archived_topic.title)
29+
end
30+
end
31+
32+
context "when searching by description" do
33+
it "only displays topics matching the search" do
34+
fill_in "search_query", with: "pharyn"
35+
36+
expect(page).to have_text(english_active_topic.title)
37+
expect(page).not_to have_text(spanish_active_topic.title)
38+
expect(page).not_to have_text(english_archived_topic.title)
39+
end
40+
end
41+
42+
context "when searching by language" do
43+
it "only displays topics matching the search" do
44+
select "Spanish", from: "search_language_id"
45+
46+
expect(page).to have_text(spanish_active_topic.title)
47+
expect(page).not_to have_text(english_active_topic.title)
48+
expect(page).not_to have_text(english_archived_topic.title)
49+
50+
select "English", from: "search_language_id"
51+
52+
expect(page).to have_text(english_active_topic.title)
53+
expect(page).to have_text(english_archived_topic.title)
54+
expect(page).not_to have_text(spanish_active_topic.title)
55+
end
56+
end
57+
58+
context "when searching by year" do
59+
it "only displays topics matching the search" do
60+
select "2025", from: "search_year"
61+
62+
expect(page).to have_text(spanish_active_topic.title)
63+
expect(page).to have_text(english_active_topic.title)
64+
expect(page).not_to have_text(english_archived_topic.title)
65+
66+
select "2023", from: "search_year"
67+
68+
expect(page).to have_text(english_archived_topic.title)
69+
expect(page).not_to have_text(spanish_active_topic.title)
70+
expect(page).not_to have_text(english_active_topic.title)
71+
end
72+
end
73+
74+
context "when searching by month" do
75+
it "only displays topics matching the search" do
76+
select "2", from: "search_month"
77+
78+
expect(page).to have_text(spanish_active_topic.title)
79+
expect(page).to have_text(english_archived_topic.title)
80+
expect(page).not_to have_text(english_active_topic.title)
81+
82+
select "3", from: "search_month"
83+
84+
expect(page).to have_text(english_active_topic.title)
85+
expect(page).not_to have_text(english_archived_topic.title)
86+
expect(page).not_to have_text(spanish_active_topic.title)
87+
end
88+
end
89+
90+
context "when searching by state" do
91+
it "only displays topics matching the search" do
92+
select "active", from: "search_state"
93+
94+
expect(page).to have_text(spanish_active_topic.title)
95+
expect(page).to have_text(english_active_topic.title)
96+
expect(page).not_to have_text(english_archived_topic.title)
97+
98+
select "archived", from: "search_state"
99+
100+
expect(page).to have_text(english_archived_topic.title)
101+
expect(page).not_to have_text(spanish_active_topic.title)
102+
expect(page).not_to have_text(english_active_topic.title)
103+
end
104+
end
105+
106+
context "when sorting" do
107+
it "displays users in the selected order" do
108+
select "asc", from: "search_order"
109+
expect(page).to have_text(/#{english_archived_topic.title}.+#{spanish_active_topic.title}.+#{english_active_topic.title}/m)
110+
end
111+
end
112+
end

spec/system/user_search_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "User search", type: :system do
4+
let(:admin) { create(:user, :admin, email: "[email protected]", created_at: 3.days.ago) }
5+
let!(:martin) { create(:user, email: "[email protected]", created_at: 2.days.ago) }
6+
let!(:rosemary) { create(:user, email: "[email protected]", created_at: 1.day.ago) }
7+
8+
before do
9+
login_as(admin)
10+
click_link("Users")
11+
end
12+
13+
it "shows by default the users from most to least recently added" do
14+
expect(page).to have_text(/#{rosemary.email}.+#{martin.email}.+#{admin.email}/m)
15+
end
16+
17+
context "when searching by email" do
18+
it "only displays users matching the search" do
19+
fill_in "search[email]", with: "mar"
20+
21+
expect(page).to have_text(rosemary.email)
22+
expect(page).to have_text(martin.email)
23+
expect(page).not_to have_text(admin.email)
24+
end
25+
end
26+
27+
context "when searching by role" do
28+
it "only displays users matching the search" do
29+
select "Admin", from: "search_is_admin"
30+
31+
expect(page).to have_text(admin.email)
32+
expect(page).not_to have_text(rosemary.email)
33+
expect(page).not_to have_text(martin.email)
34+
35+
select "Contributor", from: "search_is_admin"
36+
37+
expect(page).to have_text(rosemary.email)
38+
expect(page).to have_text(martin.email)
39+
expect(page).not_to have_text(admin.email)
40+
end
41+
end
42+
43+
context "when sorting" do
44+
it "displays users in the selected order" do
45+
select "By least recently added", from: "search_order"
46+
expect(page).to have_text(/#{admin.email}.+#{martin.email}.+#{rosemary.email}/m)
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)