Link Search Menu Expand Document

18. 資料篩選和搜尋

18-1 製作後台管理報名資料

接下來製作報名資料的後台管理頁面。

編輯 config/routes.rb,增加後台用的 registrations 路由:

   namespace :admin do
     root "events#index"
     resources :events do
+      resources :registrations, :controller => "event_registrations"

修改 app/views/admin/events/index.html.erb,加上一個連結可以前往 registration index 頁面。

+      <%= link_to "Registrations", admin_event_registrations_path(event), :class => "btn btn-default" %>
       <%= link_to "Tickets", admin_event_tickets_path(event), :class => "btn btn-default" %>

image

執行 rails g controller admin::event_registrations

編輯 app/controllers/admin/event_registrations_controller.rb,新增 index 和 destroy action

  class Admin::EventRegistrationsController < AdminController

+   before_action :find_event
+
+   def index
+     @registrations = @event.registrations.includes(:ticket).order("id DESC")
+   end
+
+   def destroy
+     @registration = @event.registrations.find_by_uuid(params[:id])
+     @registration.destroy
+
+     redirect_to admin_event_registrations_path(@event)
+   end
+
+   protected
+
+   def find_event
+     @event = Event.find_by_friendly_id!(params[:event_id])
+   end
+
+   def registration_params
+     params.require(:registration).permit(:status, :ticket_id, :name, :email, :cellphone, :website, :bio)
+   end

  end

新增 app/views/admin/event_registrations/index.html.erb

<h1><%= @event.name %> / Registrations</h1>

<p class="text-right">
  <%= link_to "New Registration", new_admin_event_registration_path(@event),
  :class => "btn btn-primary" %>
</p>

<table class="table">
  <tr>
    <th>ID</th>
    <th>Ticket</th>
    <th>Name</th>
    <th>Status</th>
    <th>E-mail</th>
    <th>建立時間</th>
    <th>Actions</th>
  </tr>
  <% @registrations.each do |registration| %>
  <tr>
    <td><%= registration.id %></td>
    <td><%= registration.ticket.name %></td>
    <td><%= registration.name %></td>
    <td><%= t( registration.status, :scope => "registration.status") %></td>
    <td><%= registration.email %></td>
    <td><%= registration.created_at %></td>
    <td>
      <%= link_to "Edit", edit_admin_event_registration_path(@event,
      registration), :class => "btn btn-default" %> <%= link_to "Delete",
      admin_event_registration_path(@event, registration), :method => "delete",
      :data => { :confirm => "Are you sure?" }, :class => "btn btn-danger" %>
    </td>
  </tr>

  <% end %>
</table>

這裡就不練習做新增和編輯了,你應該做了 100 遍了。

沒有資料不好玩,讓我們新增一些假資料。請編輯 lib/tasks/dev.rake 新增一個任務

  namespace :dev do

+   task :fake_event_and_registrations => :environment do
+     event = Event.create!( :status => "public", :name => "Fullstack Meetup", :friendly_id => "fullstack-meetup")
+     t1 = event.tickets.create!( :name => "Guest", :price => 0)
+     t2 = event.tickets.create!( :name => "VIP 第一期", :price => 199)
+     t3 = event.tickets.create!( :name => "VIP 第二期", :price => 199)
+
+     1000.times do |i|
+       event.registrations.create!( :status => ["pending", "confirmed"].sample,
+                                    :ticket => [t1,t2,t3].sample,
+                                    :name => Faker::Creature::Cat.name, 
+                                    :email => Faker::Internet.email,
+                                    :cellphone => "12345678", :bio => Faker::Lorem.paragraph,
+                                    :created_at => Time.now - rand(10).days - rand(24).hours )
+     end
+
+     puts "Let's visit http://localhost:3000/admin/events/fullstack-meetup/registrations"
+   end

執行 rake dev:fake_event_and_registrations,就會新建一個活動,會有三種票和隨機產生 1000 位報名人。

瀏覽 http://localhost:3000/admin/events/fullstack-meetup/registrations

image

18-2 分頁機制

回想看看為什麽要做分頁呢?讓我們想想看:

  • 資料庫的資料會存在硬盤上,當我們用 Ruby 程序從資料庫讀出來時,就會放在記憶體,記憶體的讀取速度比硬盤快非常非常多。所有正在執行的程序,都會放到記憶體。記憶體是有限的,例如大家的 Macbook 可能有 8G,但是硬盤空間很大,例如 (SSD)可能有 256G,傳統(磁盤式)硬盤的話現在已經發展到 1T、2T 以上,如果把硬盤的數據都讀出來放到記憶體上,不但放不下也很浪費。
  • 用戶下載所有資料需要傳輸時間,瀏覽器加載野需要時間,如果資料很多,就會花很久時間才能看到畫面。

因此,如果資料量多達數萬筆時,就一定要製作分頁的功能,避免調用 .all 方法。

我們已經教過用 will_paginate gem 來做分頁功能,這裡示範用另一套也是非常熱門的分頁套件 kaminari,兩者大同小異。可依照個人公司偏好選擇。

編輯 Gemfile

+  gem 'kaminari'

執行 bundle,重啟伺服器。

執行 rails g kaminari:views bootstrap3,這會產生搭配 Bootstrap 樣式的樣板

編輯 app/controllers/admin/event_registrations_controller.rb

   def index
-    @registrations = @event.registrations.includes(:ticket).order("id DESC")
+    @registrations = @event.registrations.includes(:ticket).order("id DESC").page(params[:page])
   end

要指定一頁有多少筆的話,使用 per 方法,例如 ` @event.registrations.includes(:ticket).order(“id DESC”).page(params[:page]).per(10)`

編輯 app/views/admin/event_registrations/index.html.erb,在最下方放上分頁的超連結。

  </table>

+  <%= paginate @registrations %>

最後結果:

image

18-3 篩選資料(單選)

需求:可以點選狀態或票種(單選)來篩選出報名人資料

image

編輯 app/views/admin/event_registrations/index.html.erb,加上兩組 Bootstrap 樣式的按鈕:

  <h1><%= @event.name %> / Registrations</h1>

  <p class="text-right">
  <%= link_to "New Registration", new_admin_event_registration_path(@event), :class => "btn btn-primary" %>
  </p>

+  <div class="submenu">
+    <div class="btn-group">
+      <%= link_to "全部 (#{@event.registrations.size})", admin_event_registrations_path(registration_filters(:status =>   nil)), :class => "btn btn-success btn-group #{(params[:status].blank?) ? "active" : ""}" %>
+      <% Registration::STATUS.each do |s| %>
+        <%= link_to t(s, :scope => "registration.status") + " (#{@event.registrations.by_status(s).size})",   admin_event_registrations_path(registration_filters(:status => s)), :class => "btn btn-success btn-group #{(  params[:status] == s) ? "active" : ""}" %>
+        <% end %>
+    </div>
+
+    <div class="btn-group">
+      <%= link_to "全部 (#{@event.registrations.size})", admin_event_registrations_path(registration_filters(:ticket_id =>   nil)), :class => "btn btn-default btn-group #{(params[:ticket_id].blank?) ? "active" : ""}" %>
+      <% @event.tickets.each do |t| %>
+        <%= link_to t.name + " (#{@event.registrations.by_ticket(t).size})", admin_event_registrations_path(  registration_filters(:ticket_id => t.id)), :class => "btn btn-default btn-group #{(params[:ticket_id].to_i == t.id)   ? "active" : ""}" %>
+      <% end %>
+    </div>
+  </div>

修改 app/helpers/admin/event_registrations_helper.rb,新增 registration_filters 方法

   module Admin::EventRegistrationsHelper
+
+   def registration_filters(options)
+     params.permit(:status, :ticket_id).merge(options)
+   end
+
  end

這個 registration_filters 方法的目的,在於建構按鈕超連結的參數。當點了狀態再點票種,或是點了票種再點狀態時,要同時套用兩個參數。

修改 app/models/registration.rb 加上兩個 scope

+  scope :by_status, ->(s){ where( :status => s ) }
+  scope :by_ticket, ->(t){ where( :ticket_id => t ) }

修改 app/controllers/admin/event_registrations_controller.rb,如果有傳參數進來,則進行篩選:

   def index
    @registrations = @event.registrations.includes(:ticket).order("id DESC").page(params[:page])

+   if params[:status].present? && Registration::STATUS.include?(params[:status])
+     @registrations = @registrations.by_status(params[:status])
+   end

+   if params[:ticket_id].present?
+     @registrations = @registrations.by_ticket(params[:ticket_id])
+   end

scope 的作用是將常用的查詢條件宣告起來,方便重復使用

修改 app/assets/stylesheets/admin.scss,調整一下間距。

+ .submenu {
+   margin-bottom: 10px;
+ }

這樣就完成了。

18-4 篩選資料(多選)

需求:可以用核選方塊(多選)狀態和票種,來篩選出報名人資料

image

實務上,單選和多選的作法不太會混用,所以這裡會註解掉上一節的單選接口讓畫面清楚一點。

再次編輯 app/views/admin/event_registrations/index.html.erb,加上核選方塊的表單,以及一個送出篩選的按鈕:

+  <% if false %>
   <div class="submenu">
     # 略
   </div>
+  <% end %>

+ <%= form_tag admin_event_registrations_path(@event), :method => :get do %>
+
+   <p><% Registration::STATUS.each do |s| %>
+     <label><%= check_box_tag "statuses[]", s, Array(params[:statuses]).include?(s) %> <%= t(s, :scope => + "registration.status") %> (<%= @event.registrations.by_status(s).size %>)</label>
+   <% end %></p>
+
+   <p><% @event.tickets.each do |t| %>
+     <label><%= check_box_tag "ticket_ids[]", t.id, Array(params[:ticket_ids]).include?(t.id.to_s) %> <%= t.name %> (<%= + @event.registrations.where( :ticket_id => t.id ).size %>)</label>
+   <% end %></p>
+
+   <p class="text-right">
+     <%= submit_tag "送出篩選", :class => "btn btn-primary" %>
+   </p>
+ <% end %>

Protip: 樣板中如果有大範圍暫時需要註解,可以用 <% if false %> …. <% end %> 包起來。

修改 app/controllers/admin/event_registrations_controller.rb,如果有傳參數進來,則進行篩選:

   def index
    @registrations = @event.registrations.includes(:ticket).order("id DESC").page(params[:page])

+   if Array(params[:statuses]).any?
+     @registrations = @registrations.by_status(params[:statuses])
+   end

+   if Array(params[:ticket_ids]).any?
+     @registrations = @registrations.by_ticket(params[:ticket_ids])
+   end

這樣就完成了。

18-5 時間區間篩選

需求:可以輸入報名日期區間來篩選出報名人資料

image

編輯 app/views/admin/event_registrations/index.html.erb,加上日期輸入框:

+  <p>
+    報名日期:<%= date_field_tag :start_on, params[:start_on] %>~<%= date_field_tag :end_on, params[:end_on] %>
+  </p>

  <p class="text-right">
    <%= submit_tag "送出篩選", :class => "btn btn-primary" %>
  </p>

修改 app/controllers/admin/event_registrations_controller.rb,如果有傳參數進來,則進行篩選:

   def index
    @registrations = @event.registrations.includes(:ticket).order("id DESC").page(params[:page])

+   if params[:start_on].present?
+     @registrations = @registrations.where( "created_at >= ?", Date.parse(params[:start_on]).beginning_of_day )
+   end

+   if params[:end_on].present?
+     @registrations = @registrations.where( "created_at <= ?", Date.parse(params[:end_on]).end_of_day )
+   end

輸入的是日期,但是資料庫中存的是 UTC 時間,因此這裡需要調用 beginning_of_dayend_of_day 才會轉換成正確的時間。例如台北時區的 2017/4/30 這一天,對資料庫中存 UTC 時間的欄位來說,正確的區間是 2017-04-30 16:00:00 UTC 到 2017-05-01 15:59:59 UTC。

這樣就完成了。

18-6 資料比對篩選

需求:可以輸入報名編號搜尋報名人資料

image

image

編輯 app/views/admin/event_registrations/index.html.erb,加上文字輸入框:


  <%= form_tag admin_event_registrations_path(@event), :method => :get do %>
+   <p><%= text_field_tag :registration_id, params[:registration_id], :placeholder => "報名編號,可用,號區隔", :class => "form-control" %></p>

修改 app/controllers/admin/event_registrations_controller.rb,如果有傳參數進來,則進行篩選:

   def index
    @registrations = @event.registrations.includes(:ticket).order("id DESC").page(params[:page])

+   if params[:registration_id].present?
+     @registrations = @registrations.where( :id => params[:registration_id].split(",") )
+   end

這樣又完成了。

18-7 關鍵字搜尋,使用 Ransack

需求:可以輸入姓名或 Email,模糊搜尋報名人資料

上一節的資料比對,會要完全一模一樣(exact match)才會篩選出來,如果只要符合部分關鍵字就好,則需要用資料庫的 LIKE 搜尋 語法,這裡我們使用 ransack gem 來幫我們達成這個功能。

編輯 Gemfile

+ gem 'ransack'

執行 bundle,重啟伺服器

修改 app/controllers/admin/event_registrations_controller.rb,改成需要先調用 ransack 方法

   def index
-    @registrations = @event.registrations.includes(:ticket).order("id DESC").page(params[:page])
+    @q = @event.registrations.ransack(params[:q])
+
+    @registrations = @q.result.includes(:ticket).order("id DESC").page(params[:page])

編輯 app/views/admin/event_registrations/index.html.erb,改成 ransack 提供的 search_form_for

-  <%= form_tag admin_event_registrations_path(@event), :method => :get do %>
+  <%= search_form_for @q, :url => admin_event_registrations_path(@event) do |f| %>
+
+    <p><%= f.search_field :name_cont, :placeholder => "姓名", :class => "form-control" %></p>
+    <p><%= f.search_field :email_cont, :placeholder => "E-mail", :class => "form-control" %></p>

其中 search_field 是 ransack 獨到的方法,後面的 :name_cont 代表 name 這個欄位要包含(contains)的關鍵字。詳見 ransack 的文檔說明,有各種用法。

image

image

ransack 會用資料庫的 LIKE 語法來做搜尋,雖然用起來方便,但它會逐筆檢查資料是否符合,而不會使用資料庫的索引。如果數據量非常多有上萬筆以上,搜尋效能就會不滿足我們的需要。這時候會改安裝專門的全文搜尋引擎,例如 Elasticsearch,這是大數據等級的。

18-8 前臺的活動狀態篩選

需求:活動的狀態有分公開(public)、私密(private)和草稿(draft):前臺的活動列表頁應該只顯示 public 的活動、前臺的活動頁面不能顯示狀態是 draft 的活動

私密(private)狀態的意思是,在活動列表上看不到,但是如果直接分享網址的話,透過連結還是可以打開。像我們預設的活動網址是隨機的 uuid,如果不是自己分享的話,是不可能猜到的。

編輯 app/models/event.rb,加上兩個 scope

+  scope :only_public, -> { where( :status => "public" ) }
+  scope :only_available, -> { where( :status => ["public", "private"] ) }

編輯 app/controllers/events_controller.rb,分別套用這兩個 scope

 class EventsController < ApplicationController

   def index
-    @events = Event.rank(:row_order).all
+    @events = Event.only_public.rank(:row_order).all
   end

   def show
-    @event = Event.find_by_friendly_id!(params[:id])
+    @event = Event.only_available.find_by_friendly_id!(params[:id])   # 如果活動是 draft 的話,經過 only_available scope 就會找不到,這就是我們的目的
   end

 end

回到前臺瀏覽看看活動列表,只剩下有公開的活動了。


Copyright © 2010-2022 Wen-Tien Chang All Rights Reserved.