Search code examples
ruby-on-railsrubyruby-on-rails-4advanced-search

Rails 4 search over several models in separate controller


I'm trying to implement a complex search over a model in a separate controller. I have a pupils model. The whole app has a front page handled by a separate main_controller which has no model. The main_controller and its related index view are supposed to provide the front page and display data from several models.

Now I want to search the model with several search criteria of different types. The search criteria are string comparisons, number comparisons and booleans (e.g. active, if true show only active pupils else show all pupils). Railscast #111 showed how to create such a search based on a model and separate search controller. I created such a controller and it works fine. I am stuck with showing the relevant parts in my main/index.

Here is the code:

main/index.html.haml

 - model_class = Adult
 - model_class = Pupil
 - model_class = MainSearch

.page-header
= render :partial => 'main_searches/form', :main_search => MainSearch.new

Just the call to the form here at the moment.

models/main_search.rb

class MainSearch < ActiveRecord::Base

  def pupils
    @pupils ||= find_pupils
  end

  private
    def find_pupils
      pupils = Pupil.order(:name_sur)
      pupils = pupils.where(id: id_search) if id_search.present?
      pupils = pupils.where("name_first like ?", "%#{name_first}%") if name_first.present?
      pupils = pupils.where("name_sur like ?", "%#{name_sur}%") if name_sur.present?
      pupils = pupils.where(active: "true") if active == true  #show only active or all
      pupils
    end
end

This defines the search.

controllers/main_searches_controller.rb

class MainSearchesController < ApplicationController
  before_action :set_main_search, only: [:show, :update, :destroy]

  def show
    @main_search = MainSearch.find(params[:id])
  end

  def new
    @main_search = MainSearch.new
  end

  def create
    @main_search = MainSearch.new(main_search_params)

    if @main_search.save
      redirect_to root_path, notice: 'Main search was successfully created.'
    else
      render action: 'new'
    end
  end
end

As shown in the railscast.

views/main_searches/_form.html.haml

%h1 Advanced Search

  = form_for :main_search, :url => main_searches_path do |f|
    .field
      = f.label :id_search
      %br/
      = f.text_field :id_search
    [... ommitted some fields here ...]
    .field
      = f.label :active
      %br/
      = f.check_box :active, {}, true, false


  .actions= f.submit "Search"

Rendered in the new view.

views/main_searches/_results.html.haml

%h1 Search Results

.container-fluid
  .row-fluid
    .span4

      %table.table{style: "table-layout:fixed"}
        %thead
          %tr
            %th= "id"
            %th= "name_sur"
            %th= "name first"
            %th= "a"
      %div{style: "overflow-y:scroll; height: 200px"}
        %table.table.table-striped{style: "table-layout:fixed"}
          %tbody
            - @main_search.pupils.each do |pupil|
              %tr
                %td= pupil.id
                %td= link_to pupil.name_sur, pupil_path(pupil)
                %td= pupil.name_first
                %td= pupil.active

Displays the results.

So basically everything works for one model as seen in the railscast. What I need now is to let the user handle everything in the main_controller somehow. At the moment I cant get the @main_search object passed to the _results.html.haml partial. What am I missing here? Or is this even the right way to do such a search?

Thanks for your help in advance.


Solution

  • Brians solution actually worked quite well. I created a separate model without a related database and put all the search logic into that file. Notice, it is only a class and not related to ActiveRecord.

    class MainSearch
    
      def self.for(id_search, name_sur, name_first, active)
    
          #escape for security
          name_sur = "%#{name_sur}%"
          name_first = "%#{name_first}%"
          active = "%#{active}%"
    
          pupils = Pupil.order(:name_sur)
          pupils = pupils.where(id: id_search) if id_search.present?
          pupils = pupils.where("name_first like ?", "%#{name_first}%") if name_first.present?
          pupils = pupils.where("name_sur like ?", "%#{name_sur}%") if name_sur.present?
          pupils = pupils.where(active: "true") if active == true  #show only active or all
      end
    end
    

    My main_controller now can call this function and pass values from the params hash:

        @pupils = MainSearch.for(params[:id_search], params[:name_sur], params[:name_first], params[:active])
    

    The parameters come from views/main/_form_search.html.haml :

    .search-box
      = form_tag root_path, method: :get do
    
        = label_tag :id_search
        %br/
        = text_field_tag 'id_search', nil
        %br/
        = label_tag :name_sur
        %br/
        = text_field_tag 'name_sur', nil
        %br/
        = label_tag :name_sur
        [...]
        = submit_tag "Submit"
    

    The final step is to show the results. As you can see in the main_controller, I just fill the @pupils object and thus can render it easily in any view. Here is the excerpt from the views/main/index.html.haml

    .span8
      .row-fluid
        %table.table{style: "table-layout:fixed"}
          %thead
            %tr
              %th= "id"
              %th= sortable "name_sur"
              %th= "name first"
              %th= "a"
        %div{style: "overflow-y:scroll; height: 200px"}
          %table.table.table-striped{style: "table-layout:fixed"}
            %tbody
              - @pupils.each do |pupil|
                %tr
                  %td= pupil.id
                  %td= link_to pupil.name_sur, pupil_path(pupil)
                  %td= pupil.name_first
                  %td= pupil.active
    
        = render :partial => 'form_search'
    

    This setup keeps all the control in the main_controller and only needs one external file (the search model). In this model I can search for data across several models (as seen in the video) and combine search criteria as I please and even do some performance shenanigans in there. I am quite happy with the result. SO thanks for all the input.