For this exercise we're going to be synthesizing all our Rails knowledge to build a album collection! At the bottom of this file you can find a link to a completed solution.
User should be able to...
- See all the albums on
albums#index
- with an album
title
andartist
- and be able to click on an individual album and be redirected to the show page
- and see a link to
Create New Album
- with an album
- See a single album on
albums#show
- with an album
title
,artist
,year
andcover_art
- with an album
- See a form to create a new album on
album#new
- Submit the new album form to
album#create
, and see the newly created album on the index page.
To create a new rails application, from the command line run:
rails new rock-n-rails-demo-app --database=postgresql --skip-test --skip-bundle
Rails uses coffee-script (.coffee
) for .js
and sass (.scss
) for .css
.
Let's keep it simple. Please remove both dependencies from your Gemfile
:
gem 'sass-rails' # remove this line
gem 'coffee-rails' # remove this line
Make sure you've saved it, and then run:
bundle install
# or
bundle
Let's also create our app's database:
rails db:create
Now you should be able to launch your local server:
rails server
# or
rails s
Now open your browser to localhost:3000
.
(STOP and COMMIT!) -- don't forget to git init
Let's take a moment to create and seed our database.
A Album
should have the following attributes:
- title — String
- artist — String
- year — Integer
- cover_art — String
- song_count — Integer
rails g model album title:string artist:string year:integer cover_art:string song_count:integer
- Run the migration that was generated to create a new table in the database.
rails db:migrate
- Play with your new
Album
model in the rails console:
rails console
> Album.all #=> []
> Album.create({title: "Test Album"})
Note that ActiveRecord displays the actual SQL query in your console. Make sure to read these queries -- it's a great way to practice!
(STOP and COMMIT)
- In
db/seeds.rb
create some albums!
db/seeds.rb
.
# Wipe the database
Album.destroy_all
# Let's create a bunch of albums
Album.create([
{
title: "On Avery Island",
artist: "Neutral Milk Hotel",
year: 1996,
cover_art: "https://upload.wikimedia.org/wikipedia/en/7/73/On_avery_island_album_cover.jpg",
song_count: 12
},
{
title: "Everything All the Time",
artist: "Band of Horses",
year: 2006,
cover_art: "https://upload.wikimedia.org/wikipedia/en/5/51/BandofHorsesEverythingalltheTime.jpg",
song_count: 10
},
{
title: "The Flying Club Cup",
artist: "Beirut",
year: 2007,
cover_art: "https://upload.wikimedia.org/wikipedia/en/4/4c/The_Flying_Club_Cup.jpg",
song_count: 13
}
])
- Run the seed file!
rails db:seed
- Check that everything was done correctly, run
rails console
or justrails c
and inside runAlbum.all
. Make sure that you can see an array of all the albums from your seed file. Exit by typingexit
.
(STOP and COMMIT)
See all the albums on albums#index
-
Visit
localhost:3000/albums
in your browser- You should see an error complaining that "no route matches...". What does that tell you?
-
Let's add our first RESTful route for our
Albums
resource!
In config/routes.rb
, add the following route(s):
get "/albums" => "albums#index", as: 'albums' # add me!
#get "/albums/new" => "albums#new", as: 'new_album'
#get "/albums/:id" => "albums#show", as: 'album'
#post "/albums" => "albums#create"
- Now, refresh the page, and you should see it complain about a missing controller!
Let's generate our albums controller!
rails g controller albums
In albums_controller.rb
let's add:
def index
render :index # optional
end
- Refresh the page, and you should see it complain about a missing view!
Let's create views/albums/index.html.erb
and add the following html:
<h1>Rock 'n Rails! (albums#index)</h1>
- Refresh the page, and you should see the above HTML rendered. Yay!
(STOP and COMMIT)
Now let's connect our model. Update your index
action in albums_controller.rb
to grab all the albums:
def index
@albums = Album.all
# render :index
end
And then let's also update the view to render a list of albums:
<% @albums.each do |album| %>
<p>Title: <%= album.title %></p>
<p>Artist: <%= album.artist %></p>
<img src="<%= album.cover_art %>"> <!-- TODO: smelly -->
<% end %>
- Refresh the page and you should see your list of albums! Good work!
(STOP and COMMIT)
See a single album on albums#show
- For each album in the
album#index
view let's create an anchor tag that will link to e.g.albums/1
,albums/2
,albums/3
views/albums/index.html.erb
.
<h1>Rock 'n Rails!</h1>
<% @albums.each do |album| %>
<p>Title: <%= album.title %></p>
<p>Artist: <%= album.artist %></p>
<img src="<%= album.cover_art %>">
<br>
<!-- anchor tag that links to a show page -->
<a href="/albums/<%= album.id %>">Show page</a> <!-- TODO: smelly -->
<% end %>
-
Now click on one of the links. They don't work yet. What error do you see?
-
Let's add our second RESTful route for our
Albums
resource!
In config/routes.rb
, add the following route(s):
get "/albums" => "albums#index", as: 'albums'
#get "/albums/new" => "albums#new", as: 'new_album'
get "/albums/:id" => "albums#show", as: 'album' # add me!
#post "/albums" => "albums#create"
-
Refresh the page. What error do you see?
-
We need to create the
albums#show
action now. And we need to grab theid
from the parameters and use it to find the matching album in the database and pass it to the view.
albums_controller.rb
# ...
def show
@album = Album.find(params[:id])
render :show #optional
end
-
Refresh the page. What error do you see?
-
Let's create
views/albums/show.html.erb
and add the following html:
<h1>Rock 'n Rails! (albums#show)</h1>
-
Refresh the page and make sure you see the HTML above rendered.
-
Now, in your
albums#show
view,views/albums/show.html.erb
display the album that is being passed in.
<img src="<%= @album.cover_art %>">
<h1><%= @album.title %></h1>
<h2>by <%= @album.artist %></h2>
<p>Year: <%= @album.year %></p>
<p>Song Count: <%= @album.song_count %></p>
<%= link_to "Back", albums_path %>
(STOP and COMMIT)
See a form to create a new album on album#new
- Let's create a link on every page that will get us to a form that creates a new album, which lives on
/albums/new
. We can edit theapplication.html.erb
file which lives inviews/layouts/
to accomplish this. Inside the file add an anchor tag just above theyield
statement in the<body>
.
<body>
<!--Every page will have this link to create a new album-->
<a href="/albums/new">Make a New Album</a> <!-- TODO: the rails way -->
<%= yield %>
</body>
When you visit localhost:3000/albums/new
, you should see an error.
- Let's add our third RESTful route for our
Albums
resource!
In config/routes.rb
, add the following route(s):
get "/albums" => "albums#index", as: 'albums'
get "/albums/new" => "albums#new", as: 'new_album' # add me! order matters!
get "/albums/:id" => "albums#show", as: 'album'
#post "/albums" => "albums#create"
-
Refresh and you should see a new error, complaining about the controller.
-
We need to create the
albums#new
action now.
albums_controller.rb
# ...
def new
render :new #optional
end
- Now we need to create
views/albums/new.html.erb
using a rails HTML form helper. Let's make all fields required.
<%= form_for @album do |f| %>
<span>Title: </span>
<%= f.text_field :title, required: true %><br>
<span>Artist: </span>
<%= f.text_field :artist, required: true %><br>
<span>Year: </span>
<%= f.number_field :year, required: true %><br>
<span>Cover art: </span>
<%= f.url_field :cover_art, required: true %><br>
<span>Song count: </span>
<%= f.number_field :song_count, required: true %><br>
<%= f.submit %>
<% end %>
- This form will not work yet. That's because we reference
@album
in the form but it's not defined. Let's define@album
in our controller and pass it into our view. All we need it to be equal to is a new instance of a theAlbum
model.
app/controllers/albums_controller.rb
# ...
def new
@album = Album.new
render :new #optional
end
- Refresh and you should see the rendered form!
(STOP and COMMIT)
Submit the new album form to album#create
to create a new album and then be redirected back to album index.
-
Now that our forms works, it will automatically
POST
to/albums
. Try it and you'll see our next error! -
Let's add our fourth RESTful route for our
Albums
resource!
In config/routes.rb
, add the following route(s):
get "/albums" => "albums#index", as: 'albums'
get "/albums/new" => "albums#new", as: 'new_album'
get "/albums/:id" => "albums#show", as: 'album'
post "/albums" => "albums#create" # add me!
- Nothing is happening in the
albums#create
controller as of yet so we need to actually create a new album there. In order to do that we must pull out the data submitted from our form from theparams
object and create a new album with it.
app/controllers/albums_controller.rb
.
def create
Album.create(
# this is known as strong parameters, and is done for security purposes
params.require(:album).permit(:title, :artist, :year, :cover_art, :song_count)
)
end
-
You may wonder what all the business is with
.require(:album).permit(...)
is. This is known as strong parameters and tells our applications these are the fields we will accept. Its good security practice to help prevent users accidentally updating sensitive model attributes. -
Additionally we can refactor this code to make it look better. We can encapsulate our strong parameter logic into a method called
album_params
. Let's make that a private method, since only the controller itself will ever use it. At the bottom ofAlbumsController
we can write:
app/controllers/albums_controller.rb
.
# public methods up here
private
def album_params
params.require(:album).permit(:title, :artist, :year, :cover_art, :song_count)
end
end # end of class
- Now our
create
method can take advantage of thealbum_params
method, which simply will output an object of key value pairs ourAlbum
model can use to create a new album. Also let's tell it to redirect to the index page once it's created the album.
app/controllers/albums_controller.rb
.
def create
Album.create(album_params)
redirect_to('/albums')
end
(STOP and COMMIT)
Congrats! We've complete all the user stories! Please see the solution branch if you have questions!
Here's a list of all of our application routes, indicating both the endpoint, the controller action, and a path prefix (i.e. the aliases we gave to our routes).
rails routes
#
# Prefix Verb URI Pattern Controller#Action
# albums GET /albums(.:format) albums#index
# POST /albums(.:format) albums#create
# new_album GET /albums/new(.:format) albums#new
# album GET /albums/:id(.:format) albums#show
#
If you're not using rails' built-in view helpers in your code, you're working too hard. Let's translate some of the "bad" code we wrote above into a cleaner, rails style:
Let's replace our <img>
tag code above with the image_tag
view helper:
<img src="<%= album.cover_art %>"> <!-- bad -->
<%= image_tag album.cover_art %> <!-- good -->
Refresh, and view the source code for your page in your browser. What does the rails helper above look like once it's converted into normal HTML?
Let's replace the <a>
tag code above with the rails-style link_to
view helper:
<a href="/albums/<%= album.id %>">Show Page</a> <!-- bad -->
<%= link_to "Show Page", "/albums/#{album.id}" %>
<!-- or -->
<%= link_to "Show Page", album_path(album.id) %>
<!-- or -->
<%= link_to "Show Page", album.id %>
<!-- or -->
<%= link_to "Show Page", album %> <!-- good -->
Refresh, and view the source code for your page in your browser. What does the rails helper above look like once it's converted into normal HTML?
Can you follow the steps outlined above to create show
and index
pages for musicians?