Practical Four: Building the Customer-Facing Side

The purpose of this practical is to extend the travel agent customer facing side by adding search/filter options for a ship, or cruise and to tie the navigation together into a more useful form.

Before doing this practical you should have finished the previous practical, because this one builds on the work you did there.

Remember that as we go through this, the assumption is that the main user is a travel agent booking a cruise on behalf of a customer.

The purpose of this practical is to look at how we can search our data and filter results back within Rails applications.

Rails or Javascript?

We’ll focus on using ruby and rails to do the work in the search and return the results to the view, which can then be filtered/sorted with Javascript. Be aware, that if you’re shifting lots of data back to the browser, that you’re better only sending what’s needed back to the browser so that you’re respecting those users who might be on mobile devices.

Our solution uses Rails for the heavy lifting of the data, instead of Javascript so that we only send back the needed data. Javascript queries will send all the data back, and then only show you what you want to see. Javascript filters the data you see on the browser side. Our solution will only send back the requested data to the browser. You can filter it further there with Javascript, but the initial query will only send back what’s requested from the database.

Navigation

Our site has limited navigation. It would be better for visitors to use the links in the menu we created previously. Open the travelagent/app/views/layouts/_menu.html.erb file and modify the line in bold:

<%= image_tag(“globeLogo.png”,  :alt =>”Travelagent.com for your travel needs” ) %>
<%= image_tag(“travelagentbanner.png”,  :alt =>”Travelagent.com is your home to arrange the perfect cruise”) %>
<%=link_to ‘Find a Cruise’, cruises_path %>
|   Make a Payment</p>

The new code will now take visitors the cruises page with our new search feature. Yes, you’re right. We could also put the search form in the menu too. I’ll leave that up to you.

Adding Search

Step 1) We want to use the cruise controller, model and views so that users can search for a specific cruise. This means we need a search form on the travelagent/app/views/cruises/index.html.erb page for people to use. Add this code to the page to create one:

<%= form_tag(cruises_path, method: :get) do %>
<%= text_field_tag :term %>
<%= submit_tag ‘Search’ %>
<% end %>

We use a ‘get’ http request so that we are only requesting information from the server, and thus don’t change anything there. This follows REST convention.

Step 2) Open up the travelagent/app/controllers/cruise_controller.rb file and amend the def cruise_params method to include :term so that it looks like this:

def cruise_params
params.require(:cruise).permit(:name, :ship_id, :term)
end

Step 3) We can also now change the index method in the controller too so that it checks for :term too. It should now look like this:

  def index
@cruises = if params[:term]
Cruise.where(‘name LIKE ?’,”%#{params[:term]}%”)
else
Cruise.all
end
end

Step 4) This should now work with a term, and an empty request should return the full list. This is fine. but it forgets your search term between requests. This is not so good. Change the code on the index.html.erb file to include this code in bold:

<%= text_field_tag :term, params[:term] %>

It should now retain your search term between requests. This also keeps all of the search code in the controller. A better version would put the search method in the model, so that we could reuse that code as needed.

Step 5) Open travelagent/app/models/cruise.rb and add the cold in bold:

class Cruise < ApplicationRecord
belongs_to :ship

def self.search(term)
    if term
      where(‘name LIKE ?’,”%#{term}%”)
    else
      all
    end
  end

end

Step 6) Change the code in the controller to simply be this:

  def index
@cruises = Cruise.search(params[:term])
end

You now have a more streamlined method in the controller with more code offloaded to the model, where it can more easily maintained following the Rails convention of ‘skinny controllers and fat models’.

This example uses the ‘name’ attribute for the search, but you could use it for other ones too, and return more complex searches across multiple attributes if that was required. Just remember to also pass through the search term as a parameterised query to keep the query more secure.

Now that you understand the basics of doing searches in Rails you should also look at theRails Guide on ActiveRecord Querying. This will offer more inspiration on how to use more complex queries to suit your specific needs, and explain examples with SQL-like precision. You can also find out more about this at RubyPlus for basic search and advanced search too.

Adding Customers

We start the customer side by adding customers and their address plus credit card. We assume this is being completed by staff at the travel agency, who are doing this on the customer’s behalf. By doing this we will also introduce a few different table relationship models.

Step 1) cd into the ‘travelagent’ directory and issue this command:

bin/rails generate scaffold Customer last_name:string first_name:string has_good_credit:integer paid:boolean

This will generate the controller called ‘CustomerController’ in the file ‘customer_controller.rb’ under travelagent/app/controllers, plus a model and assorted views for the customer too. You’ll note too, that we’re now using some new data types beyond the basic string one.

Step 2) We now need to load the migration file into the database with the command:

bin/rails db:migrate

Step 3) We can check that it works by switching to the other console, and then cd into travelagent and start the server, if it isn’t already, with the command

bin/rails server

If you’re using Cloud 9, then use this command to start the server so that you can view your pages too.

rails server -b $IP -p $PORT

Then go to your browser and navigate to http://localhost:3000/customers where we should see a space to create new customers, and then after you do so, options to show, edit, or delete each of them.

Step 4) Each customer should also have an address so that we can send them information about their cruise. We’ll associate the address with the customer model. Use this command to generate the customer address:

bin/rails generate scaffold Address street city postcode customer:references

Step 5) As before run ‘bin/rails db:migrate’ to add the table to the database:

bin/rails db:migrate

Step 6) Go to one of the page for a specific customer. You’ll see that the form for a new address also includes a space for the customer details. If you enter the ID of a previously created customer, you’ll get something like’#<Customer:0x007fca31f56628>’ which is a Ruby object reference to that instance, because we’re calling the wrong item in our code. This only works, of course, if you’ve already created a few customers. Otherwise, you’re drop-down list won’t work.

Step 7) As with ships and cabins, we need to amend the form for addresses so that we can select which customer goes with the address. Open the file travelagent/app/views/addresses/show.html.erb and find line 20:

You’ll see <%= @address.customer %> and you should change it to  <%= @address.customer.last_name %> and then refresh the page and you should see the last name of the customer whose address you created. This ‘magic’ works thanks to the relationship we created between the customer and address objects, and Ruby’s nice syntax for this sort of code. You can also do this for the travelagent/app/views/addresses/index.html.erb file too.

For now we’ll assume that only one person will have each last name, and that we can change it later to concatenate first and last names for display. We can also clean up the ‘new address’ form so that we get the correct customer when we add a new address.

Step 8) Open travelagent/app/controllers/address_controller.rb and add this line to the ‘new’ method, and the ‘edit’ method:

def new
@address = Address.new
@customers = Customer.all
end

Step 9) Then open travelagent/app/views/addresses/_form_html.erb and rewrite the<%= f.text_field :customer_id %> line:

 <%= f.label :customer_id %>

<%= f.collection_select(:customer_id, @customers, :id, :last_name ) %>

Remember the warning  about “undefined method ‘map’ “from before when we worked with linked tables:

First, If you end up with an error about “ActionView::Template::Error (undefined method `map’ for nil:NilClass)” then you have possibly used the wrong reference in your linking of tables. Places to check where you might’ve gone wrong are in the ‘new’ and ‘edit’ methods of the controller, as well as check that you’re trying to use the correct <model>_id in the private params method of allowed attributes at the end of the controller file. You might also check the <model>.rb file if you used the wrong model in the reference and tried to ‘fix’ your error by changing the files. You can also use ‘bin/rails db:rollback’ to undo a migration, then change the migration file to use the correct class, and then change the views, model, and controller and then reapply the db:migrate command.

Second, if you are using f.collection.select … , then Rails may throw a warning, and you can remove this by switching to form.collection.select… instead.

The change to the controller sends an array of customers to the view, which we can then use to generate a select field for creating addresses. By the way, we don’t need the :address symbol in our collection_select statement as that is taken care of by the ‘f’ because this is a ‘form for’ tag.  You can find more about this and other options in the API for ActionViewHelpers::FormOptionsHelper and in the RailsGuides for FormHelpers.

None of what we’ve done for the customers and addresses is pretty. However, it does show us that we’re connecting objects together correctly, and letting us see how our objects are joined together. We’ll make this all pretty at a later stage.

Step 10) Now we can add credit cards for our customers to pay for their cruises, because everyone wants their airmiles, or cash back offers. Create the credit card scaffolding with this command:

bin/rails generate scaffold CreditCard number exp_date name_on_card organisation customer:references

Step 11) Then run ‘bin/rails db:migrate’ as always after creating a model to load it into the database.

bin/rails db:migrate

As with the Address model, we want to modify the CreditCard form and views so that we see the correct customer associated with the card. Obviously, we’d do this completely differently if we were working with real cards and customers so that (a) we never stored the numbers ourselves, and (b) would not have a page that shows these details. This example is very insecure. For our learning experience we want to learn how to manage table relationships so that we can delve deeper into Ruby and Rails development in later sessions.

Step 12)  Go make the same changes to the index and show pages of under travelagent/app/views/credit_cards that you made for the Address to show the customer last_name, and to create a collection of names to match to the card.

For now we’ll also use the strange, but dangerous, ‘select a customer’ for the credit card form that we also used in the Address. While modifying the credit card details we should also change the ‘expiry date’ so that it includes month and year so that these are consistent, and that ‘organisation’ offers up ‘American Express, Visa and MasterCard’ as options. We’ll do that first as it’s the easier one.

Step 13) Open travelagent/app/models/credit_card.rbadd add this code in bold:

class CreditCard < ApplicationRecord
belongs_to :customer
validates :exp_date, :number, :name_on_card,:organisation, presence: true
enum organisation: {
    “American Express” => “AmericanExpress”,
    “Visa” => “Visa”,
    “MasterCard” => “MasterCard”
  }
end

Step 14) Go change the travelagent/app/controllers/credit_card_controller.rb to include @customers as we have in the Address_controller, and to have the travelagent/app/views/credit_card/_form.html.erb offer a select of Customers by changing this code:

  <%= f.label :organisation %>
<%= form.select :organisation,CreditCard.organisations.keys, prompt: ‘Select a card type’ %>

You’ll notice that because we are not pulling the collection from the database as we have elsewhere, Ruby makes us use ‘CreditCard.organisations.key instead of ‘organisation’ because it’s a collection of item options stored in the model. This now offers us another way to create select boxes on forms so that we always have the correct value entered into our database.

Step 15) It would also be good to have our customers be able to pick the expiry date from a calendar. We can use a built-in Rails object to do this by changing this code in the view.

<%= f.label :exp_date %>
<%= select_date Date.today, prefix: :exp_date %> “Pick any day of the month”

Alternatively, you might find that this works better for you:
<%= form.date_select :exp_date, value:Date.today %>

This will give us year, month, day, but we can throw away the parts of the date in the controller that we don’t need. This manipulation of the form input in the controller that we add below will mean that we only store the month and year values.

Step 16) Go into travelagent/app/controllers/credit_card_controller.rb and go to the ‘create’ method and add this line:

 @credit_card = CreditCard.new(credit_card_params)
@credit_card.exp_date =params[:exp_date][:year].to_s + “/” +params[:exp_date][:month].to_s

And, if you use the alternative line in the form, then you need to use this in the controller:

@credit_card.exp_date = credit_card_params[“exp_date(1i)”].to_s + “/” + credit_card_params[“exp_date(2i)”].to_s

Step 17) Also add it to the ‘update’ method so that edits to the card also work correctly.

def update
     @credit_card.exp_date =params[:exp_date][:year].to_s + “/” +params[:exp_date][:month].to_s

Step 18) Now that this is all in place for the creditcard go add a credit card to each of your customers under http://localhost:3000/credit_cards.

Wrapping Up

Before we finish we should go back and add two more things foreach class. We should put in validation for each model to check that they are complete, and we should also add the code to the controller to handle exceptions if we request an ID which doesn’t exist.

As with the ships when we checked that we had names and tonnage, and as above for the credit card code to check we had number, name on card, etc, we should do this for the customer and their address too. We won’t validate the ‘paid’ part of the customer, because they’ve either paid, or haven’t, and if we validate it, then it only works when they have paid. Remember too, that the address validation is fussy due to the relationship to the customer.

Step 1) Open up each of the customer and address models and add the following to validate the forms:

class Address < ApplicationRecord
belongs_to :customer
validates :street, :city, :postcode, presence: true
end

class Customer < ApplicationRecord
validates :last_name, :first_name,:has_good_credit, presence: true
end

This will add validation for each model. We also want our users to not have errors if they go looking for other records that don’t exist.

Step 2) Open each of the new controllers (customer, address and credit_card), and add the line in bold to handle errors:

class CustomersController < ApplicationController
before_action :set_customer, only: [:show, :edit,:update, :destroy]
rescue_from ActiveRecord::RecordNotFound,with: :redirect_if_not_found

As before we’re still routing errors to the cruise page, but that is probably ok for now and we can make it more useful later. This completes the customer side of the application. Next time, we’ll tidy things up a bit and make it all more presentable.

Beyond the Practical

Assuming that you might still have some time left, do more work on your assessment site.