Exercise 1 is about learning to read a file and exploring the array methods using the contents of the file. I’d expect to see something like this in the console after completing the exercise:
irb(main):001:0> file = File.read("test.txt") => "Call me Ishmael..." irb(main):002:0> puts file.split Call me Ishmael --snip-- => nil irb(main):003:0> puts file.split.length => 198 irb(main):004:0> puts file.split.uniq.length => 140
The output depends on the text you used.
The second exercise requires writing a little code. The following sample solves the problem using only methods covered so far:
file = File.read("test.txt") counts = {} file.split.each do |word| if counts[word] counts[word] = counts[word] + 1 else counts[word] = 1 end end puts counts
This solution should print something like this:
=> {"Call"=>1, "me"=>3, "Ishmael."=>1, ...
The word “Call” appears once in the paragraph; the word “me” appears three times; and so on.
Using the sample code provided in Exercise 3, the complete solution looks like this:
class WordCounter def initialize(file_name) @file = File.read(file_name) end def count @file.split.length end def uniq_count @file.split.uniq.length end def frequency counts = {} @file.split.each do |w| if counts[w] counts[w] = counts[w] + 1 else counts[w] = 1 end end end end
This combines the solutions to the first two exercises, wrapping them in a Ruby class.
The first exercise is about familiarizing yourself with a simple Rails application and the functionality provided by default. The address of the home page is http://localhost:3000/posts. As you move around the application, that address changes. The new post form is at /posts/new; the first post is at /posts/1; and the form for editing the first post is at /posts/1/edit. These paths and their meaning are covered in Chapter 4.
If you’ve never worked on a large application before, the number of files in a typical Rails application can seem daunting. Most editors contain some type of project list for opening files, as well as keyboard shortcuts for quickly searching for files by name. These features are invaluable when working on larger projects.
The following commands generate and run the migration to add an email address to comments:
$ bin/rails g migration add_email_to_comments email:string invoke active_record create db/migrate/20140404225418_add_email_to_comments.rb $ bin/rake db:migrate == 20140404225418 AddEmailToComments: migrating... --snip--
You can then launch a Rails console with bin/rails console
and
create a new comment with an email address.
Open app/models/comment.rb and add the validation as shown here:
class Comment < ActiveRecord::Base
belongs_to :post
validates :author, :body, presence: true
end
Note that I added the validation for both fields on a single line. You could do
this, however, with two separate calls to the validates
method.
You can’t write a single query to determine the number of comments for each post, but you can iterate over all posts and count the comments. Enter something like this in the Rails console:
2.1.0 :001 > Post.all.each do |post| 2.1.0 :002 * puts post.comments.count 2.1.0 :003 > end
This code first finds all of the posts and then makes a count query on the comments table for each one.
Open the file app/controllers/comments_controller.rb,
and find the create
method.
class CommentsController < ApplicationController
def create
@post = Post.find(params[:post_id])
if @post.comments.create(comment_params) ➊
redirect_to @post,
notice: 'Comment was successfully created.'
else
redirect_to @post,
alert: 'Error creating comment.'
end
end
--snip--
Note that it currently uses @post.comments.create(comment_params) ➊ to initialize and save the new comment as part of the if statement. You need to store the new comment in a variable so you can use the errors method to get a list of errors when the save fails. Update the create method as shown here:
class CommentsController < ApplicationController def create @post = Post.find(params[:post_id]) @comment = @post.comments.build(comment_params) if @comment.save redirect_to @post, notice: 'Comment was successfully created.' else redirect_to @post, alert: 'Error creating comment. ' + @comment.errors.full_messages.to_sentence ➊ end end --snip--
This code adds the errors to the existing alert. Notice I used the to_sentence method ➊ to convert the array of error messages to a sentence like this: “Author can’t be blank and Body can’t be blank.”
Edit app/controllers/comments_controller.rb, and find the comment_params method. Add :email to the call to the permit method:
class CommentsController < ApplicationController --snip-- private def comment_params params.require(:comment).permit(:author, :body, :email) end end
Now if a user enters an email address when adding a new comment, the address
should be stored in the database. Without this change, the email
field is
simply ignored.
Remove the h1
element from
app/views/posts/index.html.erb and update
app/views/layouts/application.html.erb, as shown here:
--snip-- <body> <h1>Listing posts</h1> <%= yield %> </body> </html>
Also change the headings in app/views/posts/new.html.erb and
app/ views/posts/edit.html.erb to h2
headings:
<h2>New post</h2>
<%= render 'form' %>
<%= link_to 'Back', posts_path %>
First, add a label and text field for :author
to the
app/views/posts/ _form.html.erb partial:
--snip-- <div class="field"> <%= f.label :title %><br> <%= f.text_field :title %> </div> <div class="field"> <%= f.label :author %><br> <%= f.text_field :author %> </div> <div class="field"> <%= f.label :body %><br> <%= f.text_area :body %> </div> --snip--
Then add :author
to the list of permitted parameters in
the post_ params
method at the bottom of
app/controllers/posts_controller.rb:
--snip-- def post_params params.require(:post).permit(:title, :author, :body) end end
Make the changes to config/routes.rb and
app/views/comments/_comment.html.erb as described in the
question. Here is how I would write the destroy
action in
app/controllers/comments_controller.rb:
--snip-- def destroy @post = Post.find(params[:post_id]) @comment = @post.comments.find(params[:id]) @comment.destroy respond_to do |format| format.html { redirect_to @post } format.json { head :no_content } end end --snip--
After editing files in your application, stage your changes in Git with git add .
, then commit these changes with
git commit -m "
Commit
Message"
, and finally push the changes to Heroku with
git push heroku master
.
If you don’t already have a GitHub account, go to https://github.com/ and complete the sign-up form. Next you’ll need to choose a plan. The free plan includes unlimited public repositories. Once you finish the sign-up process, you should see the GitHub Bootcamp screen. Follow the instructions there to create a repository and upload your application.
Create your new application in the code directory you created
in Chapter 2, not inside the blog
directory. Use the rails new
command followed by the name of your new
application. For example, to create an application to track your record collection, type
this command:
$ rails new vinyl
Next think about the models your application needs. In this case, you
probably need a Record
or Album
model. The model
needs fields such as title
, artist
, and
release_date
. Move to the vinyl directory, and
use the rails scaffold
command to generate some code to get
started:
$ cd vinyl $ bin/rails generate scaffold Album title artist release_date:datetime
Now start the Rails server and work with your new application.
In my version of Rails, the Post
class has 58 ancestors.
irb(main):001:0> Post.ancestors.count => 58
Using the Ruby pretty-print method (pp)
, you can list each
ancestor on a separate line:
irb(main):012:0> pp Post.ancestors [Post(id: integer, title: string, body: text, created_at: datetime, updated_at: datetime, author: string), Post::GeneratedFeatureMethods, #<Module:0x007fabc21bafd8>, ActiveRecord::Base, --snip-- ActiveRecord::Validations, --snip-- Kernel, BasicObject]
As you scroll through the list of ancestors, you should see some names you
recognize, such as ActiveRecord::Associations
and
ActiveRecord::Validations
. Also, notice that
Post
inherits from BasicObject
, just like every
other class in Ruby.
The
cannot_
feature
!
method should be the same as the
can_
feature
!
method except it assigns false
to the @features[f]
instead of true
.
class User FEATURES = ['create', 'update', 'delete'] FEATURES.each do |f| define_method "can_#{f}!" do @features[f] = true end define_method "cannot_#{f}!" do @features[f] = false end define_method "can_#{f}?" do !!@features[f] end end def initialize @features = {} end end
After adding this method, create another instance of the
User
class and make sure the new method works as expected:
irb(main):001:0> user = User.new => #<User:0x007fc01b95abe0 @features={}> irb(main):002:0> user.can_create! => true irb(main):003:0> user.can_create? => true irb(main):004:0> user.cannot_create! => false irb(main):005:0> user.can_create? => false
First, look at the instance methods defined by the Element
class:
irb(main):001:0> Element.instance_methods(false)
=> [:name, :name=]
The methods name
and name=
are defined as
expected. Now reopen the Element
class and add a call to
accessor :symbol:
irb(main):002:0> class Element irb(main):003:1> accessor :symbol irb(main):004:1> end => :symbol=
This should create two new methods named symbol
and
symbol=
. You can verify that the methods were created by calling
instance_methods
again:
irb(main):005:0> Element.instance_methods(false)
=> [:name, :name=, :symbol, :symbol=]
You can verify that the methods work as expected by creating an instance of the
Element
class and assigning a symbol with e.symbol =
"Au"
.
Specifying dependent: :destroy
on the
belongs_to
side of the association causes the parent model to be
destroyed when any child model is destroyed. In this example, destroying any
Post
also destroys the associated User
. This
mistake is fairly common.
The completed Comment
model should look like this:
class Comment < ActiveRecord::Base belongs_to :post belongs_to :user validates :post_id, presence: true validates :user_id, presence: true end
The Rails generator adds belongs_to
associations automatically,
but it does not add validations.
Launch the Rails console with bin/rails
console
. Create a new User
,
TextPost
, and Comment
. Verify that all of the
models were created. Then call destroy
on the new
User
and verify that the associated TextPost
and
Comment
records are also destroyed.
irb(main):001:0> carol = User.create name: "Carol" => #<User id: 3, name: "Carol", ...> irb(main):002:0> post = TextPost.create user: carol, body: "Testing" => #<TextPost id: 3, body: "Testing", ...> irb(main):003:0> comment = Comment.create post: post, user: carol, body: "Hello" => #<Comment id: 1, body: "Hello", ...> irb(main):004:0> carol.posts.count => 1 irb(main):005:0> carol.comments.count => 1 irb(main):006:0> carol.destroy ➊ --snip-- => #<User id: 3, name: "Carol", ...> irb(main):007:0> carol.posts.count => 0 irb(main):008:0> carol.comments.count => 0 irb(main):009:0> carol.reload ➋ ActiveRecord::RecordNotFound: Couldn't find User with id=3 --snip--
Note that calling destroy
on the model does not remove it from memory
➊. The variable carol
still refers to the model even though it has
been deleted from the database. Attempting to reload the model from the database raises an
ActiveRecord::RecordNotFound
exception because the record for carol has
been deleted ➋.
First, edit the text post partial at app/views/text_posts/_text_post.html.erb, as shown here:
<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> <%= text_post.title %> </h3> <%= link_to( "#{time_ago_in_words text_post.created_at} ago", post_path(text_post)) %> </div> --snip--
This creates a link to the text_post with the time in words such as “5 days ago.” Edit the image post partial at app/views/image_posts/ _image_post.html.erb with a similar change.
--snip-- </h3> <%= link_to "#{time_ago_in_words image_post.created_at} ago", post_path(image_post) %> </div> --snip--
The only difference here is the word text_post is replaced with image_post. Now load the posts index page and make sure the links work correctly.
The most important part of this exercise is restricting access to the controller to
authenticated users. Add before_action :authenticate_user!
in
app/controllers/comments_controller.rb, as shown here:
class CommentsController < ApplicationController before_action :authenticate_user! --snip-- end
The comment partial at app/views/comments/_comment.html.erb
shows the name
of the user that created the comment and the
body
of the comment.
<p><em><%= comment.user.name %> said:</em></p> <p><%= comment.body %></p>
This partial is rendered once for each comment by render
@post.comments
in the post show
view.
First, start a Rails console with bin/rails
console
to see the password_digest
for a user.
irb(main):001:0> alice = User.find 1 User Load ... => #<User id: 1, name: "Alice", ...> irb(main):002:0> alice.password_digest => "$2a$10$NBjrpHtfLJN14c6kVjG7sety1N4ifyuto7GD5qX7xHdVmbtweL1Ny"
The value of alice.password_digest
that you see will be different.
Bcrypt automatically adds a salt to the password before generating the hash digest. I
can’t tell the password for alice
by looking at that value. Bcrypt
seems pretty secure!
You can see the cookies for a site by looking at resources in your browser’s
Developer Tools or Page Info. According to the Chrome developer tools, my current
_social_session
cookie is 465 bytes of alphanumeric digits like this
"M2xkVmNTaGpVaFd..."
. Again, I’m not able to decipher that
information.
Open the TextPost
partial at
app/views/text_posts/_text_post.html.erb. It already displays the
user’s name
. Add a call to the link_to
helper method before the text_post.user.name
and also pass the
text_post.user
to the helper:
--snip-- <div class="panel-body"> <p><em>By <%= link_to text_post.user.name, text_post.user %></em></p> <%= text_post.body %> </div> --snip--
Then update the ImagePost
partial at
app/views/image_posts/_image _post.html.erb:
--snip-- <div class="panel-body"> <p><em>By <%= link_to image_post.user.name, image_post.user %></em></p> <%= image_tag image_post.url, class: "img-responsive" %> <%= image_post.body %> </div> --snip--
Finally, update the application layout at app/views/layouts/application.html.erb:
--snip-- <div class="pull-right"> <% if current_user %> <%= link_to 'Profile', current_user %> <%= link_to 'Log Out', logout_path %> <% else %> --snip--
The application layout already has a check for current_user
. Add
the Profile link inside this conditional.
Open UsersController
at
app/controllers/users_controller.rb. Requiring authentication
before the follow action is a one-line change using the authenticate_user! method you
wrote in Chapter 9.
class UsersController < ApplicationController before_action :authenticate_user!, only: :follow --snip--
The only: :follow
option means anonymous users can still access
the show, new
, and create
actions. Now update the
user show
view at app/
views/users/show.html.erb. I used two if statements to first verify that
current_user
is not nil, and then to verify that
current_user
is not equal to or already following the user being
displayed.
--snip-- <p class="lead"><%= @user.name %></p> <% if current_user %> <% if current_user != @user && !current_user.following?(@user) %> <%= link_to "Follow", follow_user_path(@user), class: "btn btn-default" %> <% end %> <% end %> <h3>Posts</h3> --snip--
You could have also done this with a single if combining all three of the conditional statements.
First, open app/controllers/image_posts_controller.rb, and add methods for the new and create actions and the private image_post_params method. These are similar to the corresponding methods in TextPostsController.
class ImagePostsController < ApplicationController def new @image_post = ImagePost.new end def create @image_post = current_user.image_posts.build(image_post_params) if @image_post.save redirect_to post_path(@image_post), notice: "Post created!" else render :new, alert: "Error creating post." end end private def image_post_params params.require(:image_post).permit(:title, :url, :body) end end
Next, add the new view at app/views/image_posts/new.html.erb:
<div class="page-header"> <h1>New Image Post</h1> </div> <%= render 'form' %>
Then add the form partial at app/views/image_posts/_form.html.erb:
<%= form_for @image_post do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: "form-control" %> </div> <div class="form-group"> <%= f.label :url %> <%= f.text_field :url, class: "form-control" %> </div> <div class="form-group"> <%= f.label :body %> <%= f.text_area :body, class: "form-control" %> </div> <%= f.submit class: "btn btn-primary" %> <%= link_to 'Cancel', :back, class: "btn btn-default" %> <% end %>
Finally, add a button to the home page at app/views/posts/index.html. erb that links to the New Image Post form:
--snip-- <p> <%= link_to "New Text Post", new_text_post_path, class: "btn btn-default" %> <%= link_to "New Image Post", new_image_post_path, class: "btn btn-default" %> </p> --snip--
Refer back to Create Post if you have any questions about these actions or views.
First, add methods for the edit
and update
actions to the ImagePostsController
at
app/controllers/image_posts_controller.rb, as shown here:
--snip-- def edit @image_post = current_user.image_posts.find(params[:id]) end def update @image_post = current_user.image_posts.find(params[:id]) if @image_post.update(image_post_params) redirect_to post_path(@image_post), notice: "Post updated!" else render :edit, alert: "Error updating post." end end private def image_post_params params.require(:image_post).permit(:title, :body, :url) end end
Next, create the edit
view at
app/views/image_posts/edit.html.erb:
<div class="page-header"> <h1>Edit Image Post</h1> </div> <%= render 'form' %>
This view uses the form partial you created in Chapter 10. Finally,
add a link to the edit
action in the ImagePost
partial at app/views/image _posts/_image_post.html.erb:
--snip-- <%= image_post.body %> <% if image_post.user == current_user %> <p> <%= link_to 'Edit', edit_image_post_path(image_post), class: "btn btn-default" %> </p> <% end %> </div> </div>
This link is wrapped in a conditional so it only appears if this image post was created by the current user.
Update the PostsController
at
app/controllers/posts_controller.rb, as shown in the
question.
--snip-- def show @post = Post.find(params[:id]) @can_moderate = (current_user == @post.user) end end
Now edit the comment partial at
app/views/comments/_comment.html.erb and add a link to destroy
the comment when the @can_moderate
instance variable is
true
:
<p><em><%= comment.user.name %> said:</em></p> <p><%= comment.body %></p> <% if @can_moderate %> <p> <%= link_to 'Destroy', comment_path(comment), method: :delete, class: "btn btn-default" %> </p> <% end %>
Be sure to add method: :delete
to the link so the
destroy
action is called. Finally, add the
destroy
action to the CommentsController
at
app/ controllers/comments_controller.rb:
--snip-- def destroy @comment = Comment.find(params[:id]) if @comment.destroy redirect_to post_path(@comment.post_id), notice: 'Comment successfully destroyed.' else redirect_to post_path(@comment.post_id), alert: 'Error destroying comment.' end end private def comment_params params.require(:comment).permit(:body, :post_id) end end
This method finds the comment, calls destroy
, and
redirects back to the post with a message indicating success or failure.
Open the routes file at config/routes.rb and edit at the
logout
route:
--snip-- get 'login', to: 'sessions#new', as: 'login' delete 'logout', to: 'sessions#destroy', as: 'logout' root 'posts#index' end
Edit the application layout at
app/views/layouts/application.html.erb and add method:
:delete
to the Log Out link.
--snip-- <div class="pull-right"> <% if current_user %> <%= link_to 'Profile', current_user %> <%= link_to 'Log Out', logout_path, method: :delete %> <% else %> --snip--
Now the link issues a DELETE request to log out of the application.
The show page loads the collection of comments to render and then loads the owner of
each comment individually as the comments are rendered. You can eager load the comments
and the owners for a post by adding includes(comments: [:user])
in
the show
method in the PostsController
at
app/controllers/posts_controller.rb:
--snip-- def show @post = Post.includes(comments: [:user]).find(params[:id]) ➊ @can_moderate = (current_user == @post.user) end end
Adding includes(comments: [:user])
tells Rails to eager load the
comments for this post and all users associated with those comments.
Open the Comment
partial at
app/views/comments/_comment.html.erb and add the cache
block:
<% cache [comment, @can_moderate] do %> ➊
<p><em><%= comment.user.name %> said:</em></p>
<p><%= comment.body %></p>
<% if @can_moderate %>
<p>
<%= link_to 'Destroy', comment_path(comment),
method: :delete, class: "btn btn-default" %>
</p>
<% end %>
<% end %>
Passing an array to the cache
method creates a cache key that
combines the elements in the array ➊. In this case, the cache key contains the
values of the comment’s id
and updated_at
fields and the value of @can_moderate
, either true or false.
Open the show page at app/views/posts/show.html.erb and add the
cache
block.
--snip-- <h3>Comments</h3> <% cache [@post, 'comments', @can_moderate] do %> ➊ <%= render @post.comments %> <% end %> --snip--
This creates a cache key that is a combination of the cache key for
@post
, the word “comments,” and the value of
@can_moderate
➊. Now the comments collection is displayed
after a single read from the cache.
You need to update the view partials for both types of posts for this exercise.
First, edit the file app/views/text_posts/_text_post.html.erb and
add a debug
call near the bottom, as shown here:
<div class="panel panel-default">
--snip--
<%= debug text_post %>
</div>
</div>
Then edit app/views/link_posts/_link_post.html.erb and
add a debug
call near the bottom:
<div class="panel panel-default">
--snip--
<%= debug link_post %>
</div>
</div>
The easiest way to add the id and type of each post to the log is by iterating over
the contents of the @posts
instance variable. Edit
app/controllers/ posts_controller.rb and update the
index
action.
class PostsController < ApplicationController before_action :authenticate_user! def index user_ids = current_user.timeline_user_ids @posts = Post.includes(:user).where(user_id: user_ids) .paginate(page: params[:page], per_page: 5) .order("created_at DESC") @posts.each do |post| logger.debug "Post #{post.id} is a #{post.type}" end end --snip--
Now when you refresh the posts index page, you should see five lines similar to “Post 5 is a TextPost” in the log.
To debug what happens when a user logs in to the application, you need to add a
debugger
call to the create action in app/controllers/
sessions_controller.rb:
class SessionsController < ApplicationController --snip-- def create debugger user = User.find_by(email: params[:email]) if user && user.authenticate(params[:password]) session[:user_id] = user.id redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end --snip--
With this line in place, you can examine the params
sent to
this action, the current contents of the session
, and the value of
user
as you move through this action.
This curl
command is the same one you used earlier to create a
new post, except I replaced the token with the word
fake
.
$ curl -i -d '{"text_post":{"title":"Test","body":"Hello"}}' -H "Content-Type: application/json" -H "Authorization: Token fake" http://localhost:3000/api/text_posts HTTP/1.1 401 Unauthorized --snip-- HTTP Token: Access denied.
Note that the status code is 401 Unauthorized and the body
contains the text "HTTP Token: Access denied."
Text posts validate the presence of a body, so use curl
to
attempt to create a text post without specifying a body.
$ curl -i -d '{"text_post":{"title":"Test"}}' -H "Content-Type: application/json" -H "Authorization: Token token" http://localhost:3000/api/text_posts HTTP/1.1 422 Unprocessable Entity --snip-- {"errors":{"body":["can't be blank"]}}
Note that the status code is 422 Unprocessable Entity and the body contains a JSON representation of the errors.
Add the show
method to
app/controllers/api/posts_controller.rb:
module Api class PostsController < ApplicationController respond_to :json --snip-- def show @post = Post.find(params[:id]) respond_with @post end end end
This method finds the requested post and assigns it to the @post
instance variable and then responds with that post. The following
curl
command verifies that this action is working:
$ curl http://localhost:3000/api/posts/1
{
"id":1,
"title":"First Post",
"body":"Hello, World!",
"url":null,
"user_id":1,
"created_at":"2014-04-22T00:56:48.188Z",
"updated_at":"2014-04-22T00:56:48.188Z"
}
Because you didn’t create a jbuilder view for this action, the default JSON representation for posts is returned.
Edit the file app/views/layouts/application.html.erb to change the title of each page:
<!DOCTYPE html> <html> <head> <title>My Awesome Site</title> --snip--
After you save this change, add it to your local Git repositories staging area, and
then commit the change with an appropriate commit
message.
$ git add . $ git commit -m "Update title"
Now deploy your change by entering bin/cap
production deploy
in your terminal.
The Ruby Toolbox at https://www.ruby-toolbox.com/ lists hundreds of gems you can use to add features to your application. For example, you can let users upload files to your application. Check the Rails File Uploads category to find several choices, including Paperclip and CarrierWave. From there, you can visit the website, read the documentation, and see the source code for each project.
Go to https://github.com/rails/rails/ to join the discussion on open issues and pull requests, and see previous commits. Ruby on Rails has a page at http://rubyonrails.org/community/ for those looking to get involved online. You can learn about upcoming Ruby and Rails conferences at http://rubyconf.org/ and http://railsconf.com,/ respectively. I hope to see you there!
18.219.134.198