Ransack with Nested Attributes

In my app, I track plates which have a number of properties. They also have a location because I need to track where each plate has been. However, for the most part, what I really care about is where the plate is now, its current location. When I do my ransack search on the plates, I want to be able to have some check boxes with the possible locations. So, for example, people could search for all the plates that were annealed at a certain temperature and are located at Chicago. This means I have to search the locations for a given plate, but only for the latest location. I tried a number of different ways to do this in ransack and didn't have much luck. I got it working how I wanted, but I'm fairly certain that this isn't the best way to do it.

Basically, I use ransack for the regular search, but then search the results to see which of those plates are in the locations marked.

In my form view

<% Location::LOCATIONS.each do |location| %>
  <%= check_box_tag 'q[locations][]', location %> <%= location %>
<% end %>

This puts a check box for each location that I have stored in my model. Right now we have about 10 locations and I doubt that we'll get many more.

With that included in my form, I'll get an array in q that holds my possible locations. So now, I just have to change my results method to check that the current locations in the list of plates and only keep those that are in the array of locations.

def results
  @q = Plate.search(params[:q])
  # Check if have any locations here, if none are checked, don't need to do anything special
  @locations = params[:q][:locations]
  if @locations.nil?
    @plates = @q.result.by_serial_number
  else
    @all = @q.result.by_serial_number
    @hold = Enumerator.new do |y|
      @all.each do |plate|
        unless(plate.locations.current_location.last.nil?)
          if (@locations.include?(plate.locations.current_location.last.location))
            y << plate
          end
        end
      end
    end
    @plates = @hold.to_a
  end

  if @plates.empty?
    redirect_to compare_path, notice: "No matching plates"
  else
    render action: "results"
  end
end

I tried making a virtual attribute (home) in my plate model, but I couldn't find a way for ransack to search based on that. In fact, I didn't have much luck finding plates that showed up at a given location at any time, not just the last location. All of my tests kept showing all the plates, so I'm sure that I'm not handling the search of locations correctly. Since each plate has_many locations, it doesn't work the same as my electroding searches. For electroding, each plate only has one. If I get more time, I'll look into this more closely and see if I can set up the search properly. But for now, this works just fine.

Ruby Enumerators

I have a rails app that handles a lot of information about a set of experimental plates. Along with holding information about each plate, I have another table locations, that's nested with the plates, as in plates has_many locations and locations belongs_to plates. The locations is a separate table because these plates can get shipped to different locations a lot. This is a way for us to track where all the plates are. Most importantly, we want to know which location for a give plate has the latest date, because this will tell us where the plate currently is.

For the most part, there are only a limited number of places where the plate will be. We have a list in the model. On our main page, I wanted to have a link to each of the main places showing which plates are at that location. Here's how I did it:

In my controller

  def plates_at_uc
    @all = Plate.by_serial_number
    @title = 'UC'
    
    @plates = Enumerator.new do |y|    
      @all.map do |plate|
        unless(plate.locations.empty?)
          if (plate.locations.current_location.last.location == 'UC')
            y << plate
          end
        end
      end
    end
    render action: "plates_by_location"
  end

I have a method for each of the locations where the plates could be. The important bit here is the enumerator that I set up. This sets up the instance variable with all the plates that I'm going to display in the view. I have to go through each plate and find its present location. If that location equals the one I want (in this case UC), I add the plate to my enumerator. I set up @title so that I can use the same view for the results and display the location in the title of the page.

Ransack Update

I have an earlier post describing the way I set up a ransack search that sets up check boxes for possible search values in a given field. This was my first time using ransack and to say I had no idea what I was doing was not too far off. I've recently had to use it for another app and took some time looking into it again. And I think I've found the correct way to do a search like this.

What I have is a table with a field called electroding_material. When filling out a form for a new object, this field is filled using a dropdown table with three choices; NiCr, Inconel, Other. In my search form, I want to have checkboxes that will let me search for say only those that are NiCr or Inconel. Here's how I did it.

My Model:

MATERIALS = ['NiCr', 'Inconel','Other']

My View:

<%= search_form_for @q, :url => results_path, :html => {:method => :post} do |f| %>
  <%= f.label :electroding_method, 'Electroding Method ' %>
<% Electroding::METHODS.each do |method| %> <%= check_box_tag 'q[electroding_method_eq_any][]', method %> <%= method %>
<%= f.submit 'Search' %> <% end %>

Important to note that I used check_box_tag and not f.check_box. Check_box is for things that are directly tied to the model. When things are not directly tied to my model (as is the case here), use check_box_tag. Another important note is the extra [] after the q[electroding_method_eq_any]. This makes sure that it sends an array of values in the search option.

My Controller:

  def results
    @q = Plate.search(params[:q])
    @plates = @q.result.by_serial_number
    
    if @plates.empty?
      redirect_to compare_path, notice: "No matching plates"
    else
      render action: "results"
    end
  end

The by_serial_number scope just orders everything by its serial number.

More Floor Work

Pulled a muscle in my back yesterday, so today was spent lying around making sure I didn't make it worse. I bought the movie "The Visitor" and watched in a large number of times. Julie always says that I like sad movies and I guess she's right. I do love that one.

Since I'm not working on the house, I thought I'd finally post the last two pictures I took when I was working on the floor so I can see the progress.

After day 4
After day 5

Floor Status

Work on sanding the floor has begun. It's painful to my knees and back and very slow. On day 1, I rented a floor sander for a few hours to do the bulk of the work. Now, I'm trying to clean things up with my little belt sander. We'll see how long it lasts before I burn out the motor.

Here are pics after each of the first three days of work. Can you spot the differences?

The Before Picture
After Day 1
After Day 2
After Day 3

Rails Forms with Check Boxes

In my latest rails app, I want to compare a number of objects. My first solution was to create a new search page with a form on it.

<%= form_tag("/compare_ids", :method => 'get') do %>
	<%= text_area_tag(:z, nil, :size => "20x6") %>
	<%= submit_tag("Compare") %>
<% end %>

The objects in question are plates, identified by a serial number. So, the user should type in the serial numbers (one per line) in the text area in the form. My compare_ids method then takes the data from the form (it comes in as a string) and finds each plate. The key is this method I have in the model. The method takes the string and breaks it up by the line breaks and then just cycles through to make a string to use in a find_by_sql command.

  def self.get_plates(list)
    @ids = list.split(/\r\n/)
    @ids.each_with_index do |id, index|
      if(index == 0)
        @string = "(plates.serial_number = '#{id}')"
      else
        @string << " OR (plates.serial_number = '#{id}')"
      end
    end
    plates = find_by_sql("select plates.* from plates where (#{@string})")
  end

This worked, but I wasn't that happy with it. Mainly because the serial numbers are kind of long and typing them in will lead to mistakes for sure.

I have another page where I list all our current plates along with their current location. I thought I should be able to make this page into a form where I put a checkbox in front of each plate. Then, the user just needs to check those to compare and hit the compare button. A much easier way of comparing a large number of plates.

My problem though was in setting up the form. I kept getting urls of the form z=123&z=456&z=789..., which lead to the value of z being the last one in the list. I wanted a way to get a string or array of values with all the values for z. Here's how I solved it.

<%= form_tag("/compare_ids", :method => 'get') do %>
  <% @plates.each do |plate| %>
    <%= check_box_tag 'z[]', "#{plate.serial_number}"%> <%= link_to "#{plate.serial_number}", plate %>
  <% end %>
<% end %>

The key here is the check_box_tag name is z[] and not just z. This will return an array. So my get_plates method above needs to be modified a little so that it can handle either a string or array. Here's the final version.

  def self.get_plates(list)
    # Given a string/array of incom serial numbers, get the info for those plates
    # The compare on plates#index returns an array, from advanced search it's a string
    if list.class == String
      @ids = list.split(/\r\n/)
    else
      @ids = list
    end
    @ids.each_with_index do |id, index|
      if(index == 0)
        @string = "(plates.serial_number = '#{id}')"
      else
        @string << " OR (plates.iserial_number = '#{id}')"
      end
    end
    plates = find_by_sql("select plates.* from plates where (#{@string})")
  end

It took me a while to figure this all out, but I'm really glad I did. My previous post on using ransack and check boxes is something I want to go back to because I think this is a much better way of handling things. I also have some more ransack stuff to do on this app and I think this will help.

Ransack Integration Test

In my efforts to test the right stuff, I wanted to test that my basic search was working properly. I have a collection of plates that we're storing a bunch of information on. Each plate has a unique serial number. We also have a unique id for each plate, but for our users, the serial number is what they'll use to identify it. So I wanted to make a form that would let people search for a particular plate based on the serial number. And since this search form will be on my home controller and not the plate controller, I need to use an integration test to do this.

I'm using ransack for search. In this case, my form has one field, so this is it:

<%= search_form_for @q, :url => search_plates_path, :html => {:method => :post} do |f| %>
<%= f.submit 'Go to Incom Serial Number' %> 
	<%= f.text_field :incom_serial_number_eq %> 
<% end %>

It works perfectly. Now I want to have a test for it.

yo:mcp maryh$ rails g integration_test basic_search
      invoke  test_unit
      create    test/integration/basic_search_test.rb

I have two fixtures and I'll search and see if I can find one of them.
text/fixtures/plates.yml

one:
  incom_serial_number: 13600-001
  incom_part_code: S203-P20-L60-O60-B08-C00
  incom_part_number: 300-2672-C

two:
  incom_serial_number: 13600-002
  incom_part_code: S203-P20-L60-O60-B08-C00
  incom_part_number: 300-3657-A

Here's what my integration test looks like.

require 'test_helper'

class BasicSearchTest < ActionDispatch::IntegrationTest
  fixtures :all  # Always use all, might need things in other fixtures

  def logger
    Logger.new(STDOUT)
  end
  
  test 'find good plate' do
    get "/home/index"
    assert_response :success
    post "/plates/search", "q" =>  { "incom_serial_number_eq" =>  "13600-002"}
    assert_response :success
    assert_template "show"
    
    # No easy way to check the table values for the result of the search
    
    # Using tag ids to enable selection, get the entry, turn it into a string,
    # split it on the line break and check that the value is correct
    serial_number = css_select("td#serial_number")
    value = serial_number[0].to_s.split("
") assert_equal '13600-002', value[1],"Serial Number wrong" part_code = css_select("td#part_code") value = part_code[0].to_s.split("
") assert_equal 'S203-P20-L60-O60-B08-C00', value[1], "Part Code wrong" part_number = css_select("td#part_number") value = part_number[0].to_s.split("
") assert_equal '300-3657-A', value[1], "Part Number wrong" end test 'do not find missing plate' do post "/plates/search", "q" => { "incom_serial_number_eq" => "1111-11"} assert_redirected_to search_path, "Not redirected correctly" assert_equal '1111-11 does not exist', flash[:notice], "Wrong flash notice" end end

I made it easy on myself by using an id in the html tags for the stuff I wanted. Then I just converted the results to a string and got it down to something I could compare. It's probably not the prettiest test ever written, but it works, so I'm happy.