Dive into the world of Ruby on Rails with this comprehensive tutorial, covering topics such as the 'has_one' and 'has_and_belongs_to_many' relationships, and exercises that will guide you through creating a working cart application.
This exercise is excerpted from Noble Desktop’s past web development training materials. Noble Desktop now teaches JavaScript and the MERN Stack in our Full Stack Development Certificate. To learn current skills in web development, check out our coding bootcamps in NYC and live online.
Note: These materials are provided to give prospective students a sense of how we structure our class exercises and supplementary materials. During the course, you will get access to the accompanying class files, live instructor demonstrations, and hands-on instruction.
Topics covered in this Ruby on Rails tutorial:
The has_one relationship, The has_and_belongs_to_many relationship
Exercise Overview
In this exercise, we’ll get the cart working with the ability to add items to it. In Rails Level 1, we explored the most basic model relationships with has_many
and belongs_to
. Now we’ll be looking at more complicated relationships between model objects.
-
If you completed the previous exercises, you can skip the following sidebar. We recommend you finish the previous exercises (8A–8D) before starting this one. If you haven’t finished them, do the following sidebar.
If You Did Not Do the Previous Exercises (8A–8D)
- Close any files you may have open.
- Open the Finder and navigate to Class Files > yourname-Rails Class
- Open Terminal.
- Type
cd
and a single space (do NOT press Return yet). - Drag the yourname-Rails Class folder from the Finder to the Terminal window and press ENTER.
- Run
rm -rf nutty
to delete your copy of the nutty site. - Run
git clone https://bitbucket.org/noble-desktop/nutty.git
to copy the That Nutty Guy git repository. - Type
cd nutty
to enter the new directory. - Type
git checkout 8D
to bring the site up to the end of the previous exercise. - Run
bundle
to install any necessary gems. - Run
yarn install --check-files
to install JavaScript dependencies.
Setup
On the Desktop, navigate to Class Files > yourname-Rails Level 2 Class > That Nutty Guy HTML
Open cart.html in a browser. This is what we’ll be building now. Customers will be able to put products in the cart and then check out.
-
For this exercise, we’ll continue working with the nutty folder located in Desktop > Class Files > yourname-Rails Class > nutty
If you haven’t already done so, we suggest opening the nutty folder in your code editor if it allows you to (like Sublime Text does).
Launch Terminal.
-
In Terminal, type
cd
and a space, then do the following:- Drag the nutty folder from Desktop > Class Files > yourname-Rails Class onto the Terminal window (so it will type out the path for you).
- In Terminal, hit Return to change directory.
-
Run the Rails server by typing the following:
rails s
In a browser, go to: localhost:3000 Feel free to check out the site. All the products should have their own images, and their specs should be nice, bulleted lists.
The Cart Controller
Switch to Terminal and hit Cmd–T to open a new tab.
-
Type the following to create a cart controller:
rails g controller cart
In your code editor, open nutty > config > routes.rb
-
A few lines above the
end
keyword, add the following code shown in bold:resources :products, only: [:index, :show] resources :cart, only: [:index, :create] root 'products#index'
Save the file, then close it.
In your code editor, open nutty > app > controllers > cart_controller.rb.
-
Add the following code shown in bold:
class CartController < ApplicationController def index @title = "Your Cart" end def create end end
-
Save the file.
The next step would typically be to create a view.
Open a Finder window and navigate to: Desktop > Class Files > yourname-Rails Class > That Nutty Guy HTML
Open the cart.html file, select all of the code between
<!-- begin cut here -->
and<!-- end cut here -->
, and hit Cmd–C to copy it.Create nutty > app > views > cart > index.html.erb and open it in your editor.
Paste the copied code.
In a browser, go to: localhost:3000/cart Looking good so far!
Click the Cart link at the top right. Notice that this took us to cart.html. We need to fix this.
In your code editor, open nutty > app > views > layouts > application.html.erb
-
Around line 43, find the link for cart.html and change it to
/cart
as shown below:<a id="cart" href="/cart">
Save the file, then close it.
In a browser, go to localhost:3000 (or reload it if you’re already there).
-
Click the Cart link and now it should take you to localhost:3000/cart
That’s the bare minimum to get us up and running. The cart data we currently have is not being driven by Rails yet.
Making the Cart Visible Only to Registered Users
First, we know we’re going to need a cart model. The question is, how are we going to know which customer a cart belongs to? To help us keep everything straight, let’s say you have to be logged in to the site to add products to the cart.
-
Go to Terminal and create a cart model by typing:
rails g model cart customer:references
The reason we’re adding
customer:references
is because we’re going to require the cart to belong to a customer account. It’s a way of making sure we only show the cart to the person it belongs to. -
Apply it to the database by typing:
rails db:migrate
In your code editor, open nutty > app > models > customer.rb
-
Add the bold code to make sure the customer model knows about the cart:
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_one :cart end
As shown in the diagram below,
has_one
is a way of saying that the Customer and Cart models are connected and the foreign key (customer_id
) is in the Cart model. If you have a one-to-one relationship (as opposed to many-to-one), you would sayhas_one
to refer from the model that doesn’t have the foreign key (Customer) over to the one that does (Cart).NOTE: The diagram above is an entity-relationship model. The line connecting the two models shows the relationship between them. In this case, the
has_one
relationship is shown by using the following line . In future diagrams in this workbook, you’ll also see the following line for ahas_many
relationship. Let’s next deal with loading the cart in the controller. Save the file, then close it.
In your code editor, open nutty > app > controllers > cart_controller.rb
-
We can only load the customer’s cart if the customer is signed in. So let’s check that before we go any further. We should also create a cart for the customer in case it doesn’t exist. Add the bold code to specify what happens if a customer is signed in or not:
class CartController < ApplicationController def index @title = "Your Cart" if customer_signed_in? current_customer.create_cart if current_customer.cart.nil? @cart = current_customer.cart else redirect_to new_customer_session_path, alert: "Please sign in to access your cart." and return end end
The first part of the code checks if the customer is signed in. If they don’t have a cart, one will be created for them. If the customer is not signed in, they will see an alert and be redirected to sign in.
and return
can be very important because otherwise, even though the customer was redirected, any code beneath it would continue executing. Save the file.
In a browser, go to: localhost:3000
Click on the Cart link at the upper right.
In order to sign in, you’ll need to create an account. Click the Sign up link.
Enter an email and password, then click Sign Up.
After signing in, go to the Cart page. You should have no problem viewing it now.
Many-to-Many Relationships
We don’t yet have a way to associate products with the Cart model. has_many
and belongs_to
are not going to be enough for us here. For one thing, where would the foreign key go? If the cart_id
key went on the products table, then each product could only ever be put in one cart, which obviously won’t work! On the other hand, putting the foreign key, like a product_id
key, in the Cart model would limit us to one product per customer. What we need to implement here is a many-to-many relationship. Rails provides a plenty of tools for us to do just that.
NOTE: For a refresher of the has_many
and belongs_to
relationship with an example of a situation in which that model relationship would be suitable, read the sidebar at the end of the exercise.
The most basic kind of many-to-many relationship is has_and_belongs_to_many
. This relationship allows each Product to have any number of carts, and each Cart to have any number of products in it. To implement this, we have to manually create a join table. A join table is a special database table, which keeps track of which products and which carts are associated.
-
Let’s create a migration to do that. In Terminal, type:
rails g migration create_carts_products cart:references product:references
NOTE: The convention for join tables in Rails is that they’re named alphabetically by model in the plural. So if you’re relating cart and product, “c” comes before “p” and make them both plural.
-
Apply the migration by typing:
rails db:migrate
In your code editor, open nutty > app > models > cart.rb.
-
Add the following bold code to let the cart model know about this relationship:
class Cart < ActiveRecord::Base belongs_to :customer has_and_belongs_to_many :products end
Save the file, then close it.
In your code editor, open nutty > app > models > product.rb.
-
We also need to let the product model know about this relationship:
validates :price, numericality: true has_and_belongs_to_many :carts has_one_attached :image
That’s enough to create a many-to-many relationship between those two model objects.
Save the file and then close it.
Making the Add to Cart Button Functional
Let’s turn the Add To Cart button into a proper form so we can put items in the cart.
In a browser, go to: localhost:3000
Click on Tinfoil Hat.
-
Check out the Add To Cart button. Of course it doesn’t work yet, but we’re about to change that. To do that, we’re going to add a simple form.
While building the Flix site, we used Rails’s
form_with model: @movie
helper to build a form around the movie model. In this case, we don’t necessarily have a cart yet—we don’t even know if the user is logged in. Fortunately Rails provides another approach to forms—one that doesn’t require a model object to work. This uses theform_with url:
helper. In your code editor, open nutty > app > views > products > show.html.erb
-
Around line 25, add the following bold code:
<p class="star-rating-large five ir">Star Rating</p> <%= form_with url: '/cart', method: :post do %> <% end %> <input type="number" name="quantity" min="1" max="100" value="1">
When submitted, this form will send the contents of our form to ‘/cart’ using the POST method.
IMPORTANT TO KNOW: POSTing to the root of a controller’s route calls the
create
method in cart_controller. That’s why we enabled the:index
and:create
methods inroutes.rb
earlier. -
Add the following bold code:
<%= form_with url: '/cart', method: :post do %> <%= hidden_field_tag :product_id, @product.id %> <%= number_field_tag :quantity, 1, min: 1, max: 100 %> <%= button_tag id: 'add-cart', class: 'btn btn-red btn-md' do %> <span class="glyphicon glyphicon-shopping-cart"></span> Add to Cart <% end %> <% end %>
NOTE: The
hidden_field_tag
lets the form know which product goes in the cart. Thenumber_field_tag
creates a field for the quantity. Thebutton_tag
is our submit button. -
Delete the old cart button code shown in bold below (around lines 32–37):
<input type="number" name="quantity" min="1" max="100" value="1"> <a href="cart.html"> <button type="button" id="add-cart" class="btn btn-red btn-md"> <span class="glyphicon glyphicon-shopping-cart"></span> Add to Cart </button> </a>
Save the file.
Making Sure Customers Are Signed In
-
In your code editor, open nutty > app > controllers > cart_controller.rb
Before a customer can add anything to their cart, they need to have a cart. Before they can have a cart, they need to be signed in. We could check if they are signed in by adding code to the
create
method… but that wouldn’t be very DRY because we would be repeating code. -
Instead, cut (Cmd–X) the following bold code:
class CartController < ApplicationController def index @title = "Your Cart" if customer_signed_in? current_customer.create_cart if current_customer.cart.nil? @cart = current_customer.cart else redirect_to new_customer_session_path, alert: "Please sign in to access your cart." and return end end
-
Between the
create
method and the finalend
keyword, create a private (internal controller) method as shown in bold below:def create end private def load_cart_or_redirect_customer end end
-
Paste (Cmd–V) the code you cut into the method as shown below:
private def load_cart_or_redirect_customer if customer_signed_in? current_customer.create_cart if current_customer.cart.nil? @cart = current_customer.cart else redirect_to new_customer_session_path, alert: "Please sign in to access your cart." and return end end
NOTE: Remember that private controller methods don’t necessarily lead to a page—they are just called internally by the controller.
-
We want the private method to run before the
index
orcreate
pages so the sign in code will run before customers view the cart or add anything to it. Add this code:class CartController < ApplicationController before_action :load_cart_or_redirect_customer def index
Adding Products to the Cart
-
Now we can load up the cart with products. Around line 9, type:
def create product = Product.find(params[:product_id]) @cart.products << product @cart.save redirect_to '/cart', notice: "#{product.title} was added to your cart." and return end
NOTE: Remember, from show.html.erb, we’re going to pass ourselves the product’s id as a hidden field so we will get it back in the
params
hash. The product is then added to the cart. Then the cart is saved. Finally the customer is redirected to the cart page with a notice that the product was added successfully. Save the file.
Let’s try it out and see if it works. In the browser, go back to the Tinfoil Hat page and reload it.
-
Click Add To Cart.
It’s working, but our cart is still showing the sample data. We need to implement display of the actual items in the cart.
In your code editor, open nutty > app > views > cart > index.html.erb
-
Around lines 20 and 36, wrap the existing code in the following bold tags:
<tbody> <% @cart.products.each do |product| %> <tr> <td id="thumbnail-div" class="hidden-xs">
Code Omitted To Save Space
<td class="hidden-xs">$19.99</td> <td class="total-price">$19.99</td> </tr> <% end %> <td id="thumbnail-div" class="hidden-xs">
-
We only need one row to make the cart show the correct item(s). Before we make that row dynamic, first delete the extra row (around lines 37–49):
<td id="thumbnail-div" class="hidden-xs"> <a href="#"><img src="img/product_images/toothbrush.jpg" class="cart-thumbnail" alt="Pantone Toothbrush"></a> </td> <td> <a href="#"><p>Pantone Toothbrush Set</p></a> <p class="gray-text">Item #NG00921</p> </td> <td><input type="number" name="quantity" min="1" max="100" value="2"> <a href="#"><span class="glyphicon glyphicon-refresh"></span>update</a> <a href="#"><span class="glyphicon glyphicon-remove"></span>remove</a> </td> <td class="hidden-xs">$9.99</td> <td class="total-price">$19.98</td>
-
Around line 23, highlight the link and image:
<a href="product.html"><img src="img/product_images/tinfoil-hat.jpg" class="cart-thumbnail" alt="Tinfoil Hat"></a>
-
Delete it and replace it with the bold code shown below:
<td id="thumbnail-div" class="d-none d-md-table-cell"> <%= link_to product do %> <%= image_tag url_for(product.image.variant(resize_to_limit: [200, 200])), alt: product.title, class: 'cart-thumbnail' %> <% end %> </td>
-
Around line 28, find the following code and highlight it:
<a href="product.html"><p>Tinfoil Hat</p></a>
-
Replace it with the bold code as shown:
<td> <%= link_to product do %> <p><%= product.title %></p> <% end %> <p class="gray-text">Item #NG45636</p>
-
Replace the item number as shown (around line 31):
<p class="gray-text">Item #<%= product.sku %></p>
We’ll implement the quantity, update, and remove actions later.
-
Replace the product prices ($7.99) as shown (around lines 37–38):
<td class="d-none d-md-table-cell"><%= number_to_currency product.price %></td> <td class="total-price"><%= number_to_currency product.price %></td>
Save the file.
-
Go back to the cart in the browser and reload it: localhost:3000/cart
Now it should only have Tinfoil Hat in it.
Try adding another product. Click on the logo at the top left to go home.
-
Click any product to go to its page. Then click the Add To Cart button.
Voilà! Now we should have the two items we added to our cart!
How Do We Implement the Quantity Field?
-
While we’re still looking at the cart in the browser, take a look at the Quantity field. We haven’t done anything to make it functional yet.
How do we implement this Quantity field? Does it become a property of the product? That wouldn’t make sense. Does it become a property of the cart?
As we strategize what to do about the Quantity field, we begin to see the problem with the
has_and_belongs_to_many
relationship. It’s sufficient for the most basic many-to-many relationships. However, we’re already at an impasse here in our implementation: we need to tack on some additional metadata to the relationship between two models.The Quantity field refers to the relationship of the product and the cart. So we somehow need to get the quantity into the join table we created in this exercise. With a simple join table that isn’t a model object of its own, there’s no room there to do it.
In the next exercise, we’ll look at a more powerful way to handle these kinds of relationships.
Leave your code editor and browser open, as well as the server running in Terminal so we can continue with them in the following exercises.
The has_many & belongs_to Relationship
One of the most basic model relationships in Ruby on Rails is one in which one model has_many
instances of objects from another model (which belongs_to
the model that has_many
). It’s just like the has_one
relationship introduced earlier in this exercise, except that objects from one model will need to be associated with more than one object (even a lot of them!) from the connected model.
The diagram below references a movie database app that has two connected models. Each movie in the app has a unique numeric id field, which indicates that each movie only appears once in the database. That’s your indicator that the Movie model is the one that has_many
.
In this app, the goal is to give the site’s admins the ability to assign as many different actors (cast members) to each movie as the cast roster calls for. To create that functionality, we would make sure the Cast Member model belongs_to
the Movie model. The Cast Member model gets a foreign key which says that it belongs to the Movie model, the one that has_many
.
The foreign key movie_id
refers to an integer field that stores the unique number associated with each item in the Movie model. This field stores the id of the movie each cast member is associated with. It is called “foreign” because it refers to another table, and called a “key” because it refers to that table’s primary key (id).