Learn how to enhance your Ruby on Rails skills with this comprehensive tutorial that covers topics such as creating a model method for runtime, Scopes, DRYing up the scopes, and adding tab highlight behavior.
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.
Topics covered in this Ruby on Rails tutorial:
Creating a model method for runtime, Scopes, Optional bonus: DRYing up the scopes, Additional bonus: adding the tab highlight behavior
Exercise Preview
Photo courtesy of istockphoto, © Bliznetsov, Image #20982716
Exercise Overview
Perhaps over the course of writing validations you started thinking, “We have this nice big model file, movie.rb, and it’s so empty! What else can we put in it?” For one: model methods, which, like the name suggests, pertain to behaviors of models and their components.
Another popular saying in the Rails community is “fat model, skinny controller”—in other words, the bulk of an application’s logic should be in its models, not in its controllers or views. The assumption underlying this principle is that model code is more reusable than controller code, thus more DRY.
-
If you completed the previous exercises, you can skip the following sidebar. We recommend you finish the previous exercises before starting this one. If you haven’t finished them, do the following sidebar.
If You Did Not Do the Previous Exercises (3A–4D)
- 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 flix
to delete your copy of the Flix site. - Run
git clone https://bitbucket.org/noble-desktop/flix.git
to copy the Flix git repository. - Type
cd flix
to enter the new directory. - Type
git checkout 4D
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.
Getting Started
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 flix folder from the Finder to the Terminal window.
Make sure you’re in Terminal and hit Return to change into the new folder.
-
Type the following in Terminal:
rails server
The Rails server is now running, so let’s get to work exploring model methods.
Creating a Model Method for Runtime
The Flix site currently has runtimes listed in minutes—but honestly, who wants to mentally calculate the equivalent of 238 minutes? Let’s make these values more user-friendly by displaying them in hours and minutes. This would be a good use for a model method.
We suggest opening the flix folder in your code editor if it allows you to (like Sublime Text does).
In your code editor, open flix > app > models > movie.rb
-
Let’s add a new method to the model file. Type the following above the last
end
:def runtime_hours unless runtime.nil? end end u
We already have a value called
runtime
, so we have to name this one something different, likeruntime_hours
. Also, it’s always a good idea to add anunless
statement right away; in the case that there is no runtime entered for a given movie, we’ll avoid an error that will crash the whole page. -
Add the following bold code to the
unless
statement:unless runtime.nil? "#{runtime / 60} hrs." end
A Rails quirk to be aware of: when Rails divides using / it completely discards the remainder of the division equation. Conversely, the modulus (with %) produces ONLY the remainder of the division equation; let’s use it to take care of the minutes.
-
Add the following bold code to the line you just added:
"#{runtime / 60} hrs. #{runtime % 60} min."
Save the file.
Open app > views > movies > index.html.erb
-
Find the following code, around line 22:
<div><%= movie.mpaa_rating %>, <%= movie.runtime %> minutes</div>
-
Edit the line as shown below, deleting the word
minutes
because we added that in theruntime_hours
model method:<div><%= movie.mpaa_rating %>, <%= movie.runtime %></div>
-
Add the following bold code:
<div><%= movie.mpaa_rating %>, <%= movie.runtime_hours %></div>
Save the file.
We need to make the same change in the show file. Open app > views > movies > show.html.erb
-
Find the following piece of code, around line 8:
<p class="runtime">Runtime: <%= @movie.runtime %> minutes </p>
-
Edit the code as follows, making sure to delete the word
minutes
:<p class="runtime">Runtime: <%= @movie.runtime_hours %></p>
Save the file.
-
Switch to the browser, navigate to localhost:3000 and notice that all of the runtimes now display in user-friendly hours and minutes!
What other methods could we add to the model? Well, we’ve listed MPAA ratings in a number of places—including the controller, where they don’t really belong! Let’s DRY up the code by storing the complete list of MPAA ratings as a class method in the model.
DRYing Up the Code with Another Model Method
In your code editor, open app > views > movies >
_
form.html.erb-
Find the following code, around line 41:
<% mpaa_ratings = options_for_select ["G", "PG", "R", "NR"], selected: @movie.mpaa_rating %>
-
Select the following portion, the array of MPAA ratings, and cut it (Cmd–X):
["G", "PG", "R", "NR"]
-
The remaining code should look like this:
<% mpaa_ratings = options_for_select(, selected: @movie.mpaa_rating) %>
-
Add the name of the class method we’re about to create:
<% mpaa_ratings = options_for_select(Movie.all_mpaa_ratings, selected: @movie.mpaa_rating) %>
Remember, class methods always start with the name of the class itself.
Save the file.
Switch to movie.rb in your code editor.
-
Add the following bold code above the start of the mpaa_rating validation method:
def self.all_mpaa_ratings end validates :mpaa_rating, inclusion: { in: self.all_mpaa_ratings }
You may be wondering, why did we use
self
here, and not earlier? Well, because the list of possible MPAA ratings is common to all movies and not particular to any one movie, we are creating a class method here rather than an instance method. Class methods are called by invoking the class directly, rather than the particular instance. So when we want to invoke the list of possible MPAA ratings, we can callMovie.all_mpaa_ratings
and we don’t need to rely on a particular instance of the movie class.You may also be wondering, why does this method come above the validation? The answer is because the validation needs this method to exist first—otherwise you’ll get an error.
-
Paste the code within the method so the code reads as follows:
def self.all_mpaa_ratings ["G", "PG", "R", "NR"] end
We decided to make a class method because there is just one list that will be the same for every movie; we only would have made an instance method if there were somehow a different list of MPAA ratings for each movie.
-
Finally, we have the opportunity to change the array syntax to a simpler version, so let’s do it!
def self.all_mpaa_ratings %w(G PG R NR) end
This alternative Ruby array syntax can only be used when your array is comprised of simple strings, without spaces.
Scopes
Another way to add logic to your app, besides model methods, is to utilize scopes. Scopes provide a handy way to select only the records that match a specific pattern.
-
In a browser, navigate to localhost:3000 and try clicking the tabs that say All Movies, In Theaters, Coming Soon, and Go Now. The tabs currently lack functionality, but we can get them working with scopes!
We already have a field to handle this element (placement), which contains information about each movie’s placement category. We just need to create a scope for the record and a bit of controller logic to route the movies appropriately.
Switch to your code editor.
Open flix > config > routes.rb
-
On line 4, define a new route as follows:
get 'movies/recommended/:placement' => 'movies#recommended' resources :movies
This’ll pass a parameter called :placement
to the controller action recommended
.
Save the file.
Open app > controllers > movies_controller.rb
-
Add the following method around line 11. It doesn’t really matter where it goes, in fact, as long as it is above the private method:
def recommended @placement = params[:placement] end
We created the instance variable
@placement
in case we wanted to display it on the screen or use it for another purpose. Because we have three placement values, and we want it to do something different for each of those values, this is a good opportunity to use a case statement. -
Add the following bold code to create a case statement:
def recommended @placement = params[:placement] case @placement when 'in_theaters' when 'coming_soon' when 'go_now' end render 'index' end
You might be wondering: isn’t this where we would create a new view file, e.g.
app/views/movies/placement.html.erb
? But, why create duplicate code? We already have a view (the index page) that is capable of showing a list of movies as a grid. So, we’re using therender
command to ask Rails to just use an existing view.We left the three cases blank because we haven’t written the scopes yet. Let’s create them now.
Save the file.
Switch to movie.rb
-
Add the following code around line 6:
validate :mpaa_rating_must_be_in_list scope :in_theaters, def runtime_hours
A scope definition always starts with the word
scope
followed by the name of the scope (in this case,:in_theaters
). -
Add the following code to the scope definition:
scope :in_theaters, -> { where(placement: 'in_theaters') }
The little arrow
->
is referred to as a lambda and indicates that some Ruby code is about to follow. We can then invoke the Rubywhere
method to find the exact movies we want to display. This scope tells Rails to load all movies where placement is equal toin_theaters
. Let’s define the other two scopes. -
Copy the entire line you just wrote and paste it twice below (or in Sublime Text you can duplicate the current line by pressing Cmd–Shift–D) so you have a total of three identical definitions as follows:
scope :in_theaters, -> { where(placement: 'in_theaters') } scope :in_theaters, -> { where(placement: 'in_theaters') } scope :in_theaters, -> { where(placement: 'in_theaters') }
-
Make the following changes shown in bold:
scope :in_theaters, -> { where(placement: 'in_theaters') } scope :coming_soon, -> { where(placement: 'coming_soon') } scope :go_now, -> { where(placement: 'go_now') }
Save the file.
Switch back to movies_controller.rb
-
Add the following bold code to the case statement:
@placement = params[:placement] case @placement when 'in_theaters' @movies = Movie.in_theaters when 'coming_soon' @movies = Movie.coming_soon when 'go_now' @movies = Movie.go_now end render 'index'
This means that when the placement is
in_theaters
, the@movie
instance variable should be equal toMovie.in_theaters
, and so on for the other cases. Save the file.
Last but not least, let’s update the index view. Switch back to index.html.erb.
-
Find the code for the tabs, which starts around line 7:
<li class="active"><a href="#">All Movies</a></li> <li><a href="#">In Theaters</a></li> <li><a href="#">Coming Soon</a></li> <li><a href="#">Go Now</a></li>
-
Delete the opening and closing anchor tags so the code looks like this:
<li class="active">All Movies</li> <li>In Theaters</li> <li>Coming Soon</li> <li>Go Now</li>
-
Right now the All Movies tab is always highlighted in orange. Remove the
class="active"
code as follows:<li>All Movies</li>
-
Add the following bold embedded Ruby code to the All Movies tab:
<li><%= link_to " All Movies", movies_path %></li>
We are using the
link_to
helper. As you can see from the code, thelink_to
helper first takes the text for the link, then takes the link itself.You might be wondering where the
movies_path
part came from; it’s the same as typing"/movies"
. Remember, to see all the existing routes in the application you can switch to the Terminal and typerake routes
. Append_path
to any of the listed routes and a little bit of Rails magic will happen; Rails will know the exact path to that particular route! There is no route forrecommended
, though, so we’ll have to write out the other links longhand. -
Add the embedded Ruby for the other links as follows:
<li><%= link_to "All Movies", movies_path %></li> <li><%= link_to " In Theaters", "/movies/recommended/in_theaters" %></li> <li><%= link_to " Coming Soon", "/movies/recommended/coming_soon" %></li> <li><%= link_to " Go Now", "/movies/recommended/go_now" %></li>
Save the file.
-
Switch to the browser, navigate to localhost:3000/movies and click on each tab, which should display certain movies based on their placement value. Great!
That was a pretty easy feature to add to the site, primarily because we didn’t have to modify the logic of the index view. In fact, let’s look at the index view a bit closer.
-
Switch back to index.html.erb in your code editor.
This view was written in a fairly generic way. It simply expected to receive an instance variable called
@movies
which contained some number of movie objects, as you can see in line 15:<% @movies.each do |movie| %>
The view doesn’t care whether
@movies
contains all movies, or just a subset, as long as each object is a movie. It’s the controller’s job to indicate precisely which collection of movies is shown. Let’s take a quick look at the controller code. -
Switch to movies_controller.rb and take a look at this method around line 7:
def index @movies = Movie.all end
This means that by default, all movies should be sent to the index.
-
Look at the
recommended
method that we wrote a few minutes ago (it starts around line 11).This method allows the controller to receive user input (i.e., whichever tab they select) and make a decision about which group of movies to feed back to the view. The view then goes on to display what it’s told to display. This is an example of MVC all working together in harmony!
Back in Terminal hit Ctrl–C to shut down the server.
Optional Bonus: DRYing Up the Scopes
We did a really good job here leveraging the power of Rails to create this feature… but we can always do better. We have three very similar-looking scopes; let’s DRY them up into one simplified scope.
Switch to movie.rb in your code editor.
-
Delete the scopes for
coming_soon
andgo_now
so this is the only one left:scope :in_theaters, -> { where(placement: 'in_theaters') }
-
Make the following substitutions shown in bold:
scope :with_placement, -> { where(placement: placement) }
-
Add the following bold code after the lambda:
scope :with_placement, -> (placement) { where(placement: placement) }
This is a really idiosyncratic bit of Rails syntax, meaning that the scope receives
placement
as a variable.We have just replaced three scopes that corresponded to specific placement values with one scope that can take any value for placement, receive it as a variable and filter the set of models by placement value. This level of abstraction allows us to DRY up the code easily.
Save the file.
Switch to movies_controller.rb
-
Find the following area of code:
def recommended @placement = params[:placement] case @placement when 'in_theaters' @movies = Movie.in_theaters when 'coming_soon' @movies = Movie.coming_soon when 'go_now' @movies = Movie.go_now end render 'index' end
-
Delete the case statement so the code reads as follows:
def recommended @placement = params[:placement] render 'index' end
-
Add the following bold code:
def recommended @placement = params[:placement] @movies = Movie.with_placement(@placement) render 'index' end
Notice that we are able to call the scope
with_placement
just like we would call a regular method. Save the file.
-
Switch to Terminal and type the following to launch the server:
rails server
Switch to the browser, reload localhost:3000/movies and click on any of the tabs. They should still be working perfectly, but now our code is much DRYer.
Back in Terminal hit Ctrl–C to shut down the server.
Additional Bonus: Adding the Tab Highlight Behavior
Let’s get the tab highlight behavior working properly so the placement tabs turn orange when selected.
Switch to index.html.erb in your code editor.
-
Find the following code, starting around line 7 and add the bold changes. Make sure to nest these within the
<li>
tags and type a single space before eachclass=active;
(The spaces below are not typos.)<li<%= ' class=active' if @placement.nil? %>><%= link_to "All Movies", movies_path %></li> <li<%= ' class=active' if @placement == "in_theaters" %>><%= link_to "In Theaters", "/movies/recommended/in_theaters" %></li>
-
Copy and paste the
in_theaters
bit of code as shown below:<li<%= ' class=active' if @placement.nil? %>><%= link_to "All Movies", movies_path %></li> <li<%= ' class=active' if @placement == "in_theaters" %>><%= link_to "In Theaters", "/movies/recommended/in_theaters" %></li> <li<%= ' class=active' if @placement == "in_theaters" %>><%= link_to "Coming Soon", "/movies/recommended/coming_soon" %></li> <li<%= ' class=active' if @placement == "in_theaters" %>><%= link_to "Go Now", "/movies/recommended/go_now" %></li>
-
Make the changes shown in bold below:
<li<%= ' class=active' if @placement.nil? %>><%= link_to "All Movies", movies_path %></li> <li<%= ' class=active' if @placement == "in_theaters" %>><%= link_to "In Theaters", "/movies/recommended/in_theaters" %></li> <li<%= ' class=active' if @placement == "coming_soon" %>><%= link_to "Coming Soon", "/movies/recommended/coming_soon" %></li> <li<%= ' class=active' if @placement == "go_now" %>><%= link_to "Go Now", "/movies/recommended/go_now" %></li>
NOTE: Rails automatically (and somewhat inexplicably) wraps
class=active
in quotes (so it looks likeclass="active"
) which saves us a little bit of work. Save the file.
-
Switch to Terminal and type the following to launch the server:
rails server
Switch to the browser and navigate to localhost:3000
Click on the placement tabs. They should work perfectly, turning orange when selected. The Flix site is really coming along!
Back in Terminal hit Ctrl–C to shut down the server.