Ch 13: User microposts Flashcards

You may prefer our related Brainscape-certified flashcards:
1
Q

Create topic branch

A

Create topic branch

git checkout -b user-microposts

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

generate the Micropost model

A

generate the Micropost model

rails generate model Micropost content:text user:references

  • the generated model includes a line indicating that a micropost belongs_to a user, which is included as a result of the user:references argument
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

Add index to several columns

A

Add index to several columns

db/migrate/[timestamp]_create_microposts.rbclass CreateMicroposts < ActiveRecord::Migration[5.0] def change create_table :microposts do |t| t.text :content t.references :user, foreign_key: true t.timestamps end add_index :microposts, [:user_id, :created_at] endend

  • Because we expect to retrieve all the microposts associated with a given user id in reverse order of creation, the code adds an index on the user_id and created_at columns:

add_index :microposts, [:user_id, :created_at]

  • By including both the user_id and created_at columns as an array, we arrange for Rails to create a multiple key index, which means that Active Record uses both keys at the same time.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Write test for microposts

A

Write test for microposts

test/models/micropost_test.rbrequire 'test_helper'class MicropostTest < ActiveSupport::TestCase def setup @user = users(:michael) # This code is not idiomatically correct. @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id) end test "should be valid" do assert @micropost.valid? end test "user id should be present" do @micropost.user_id = nil assert_not @micropost.valid? endend

  • In the setup step, we create a new micropost while associating it with a valid user from the fixtures, and then check that the result is valid.
  • Because every micropost should have a user id, we’ll add a test for a user_id presence validation.
  • the code to create the micropost is not idiomatically correct but we’ll fix it later
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

validate for presence of user ID for microposts

A

validate for the presence of user ID for microposts

app/models/micropost.rbclass Micropost < ActiveRecord::Base belongs_to :user validates :user_id, presence: trueend

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Add tests for validations

A

Add tests for validations

test/models/micropost_test.rbrequire 'test_helper'class MicropostTest < ActiveSupport::TestCase... test "content should be present" do @micropost.content = " " assert_not @micropost.valid? end test "content should be at most 140 characters" do @micropost.content = "a" * 141 assert_not @micropost.valid? endend

  • the code uses string multiplication to test the micropost length validation:
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Add validations for the micropost’s content attribute

A

Add validations for the micropost’s content attribute

app/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user validates :user_id, presence: true validates :content, presence: true, length: { maximum: 140 }end

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

replace the idiomatically incorrect code

A

replace the idiomatically incorrect code

  • from this

@user = users(:michael)# This code is not idiomatically correct.@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)

  • to this

@user = users(:michael)@micropost = @user.microposts.build(content: "Lorem ipsum")

  • When a new micropost is made in this way, its user_id is automatically set to the right value.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

Using the belongs_to/has_many association defined in this section, Rails constructs these methods

A

Using the belongs_to/has_many association defined in this section, Rails constructs these methods

Micropost.createMicropost.create!Micropost.new

Therefore we have

user.microposts.createuser.microposts.create!user.microposts.build

When a new micropost is made in this way, its user_idis automatically set to the right value.” />

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

Association methods

A

Association methods

Method | Purposemicropost.user | Returns the User object associated with the micropostuser.microposts | Returns a collection of the user’s micropostsuser.microposts.create(arg) | Creates a micropost associated with useruser.microposts.create!(arg) | Creates a micropost associatedwith user (exception on failure)user.microposts.build(arg) | Returns a new Micropost object associated with useruser.microposts.find_by(id: 1) | Finds the micropost with id 1 and user_id equal to user.id

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

update the User and Micropost models with code to associate them together

A

update the User and Micropost models with code to associate them together

  • This will allow us to get code like @user.microposts.build to work
  • Micropost belongs to user”

app/models/micropost.rb class Micropost < ApplicationRecord belongs_to :user validates :user_id, presence: true validates :content, presence: true, length: { maximum: 140 } end

  • User has many microposts

app/models/user.rbclass User < ApplicationRecord has_many :microposts . . .end

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

update the setup method with the idiomatically correct way to build a new micropost

A

update the setup method with the idiomatically correct way to build a new micropost

test/models/micropost_test.rbrequire 'test_helper'class MicropostTest < ActiveSupport::TestCase def setup @user = users(:michael) @micropost = @user.microposts.build(content: "Lorem ipsum") end test "should be valid" do assert @micropost.valid? end test "user id should be present" do @micropost.user_id = nil assert_not @micropost.valid? end . . .end

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

write a test to verify that the first micropost in the database is the same as a fixture micropost we’ll call most_recent

A

write a test to verify that the first micropost in the database is the same as a fixture micropost we’ll call most_recent

test/models/micropost_test.rbrequire 'test_helper'class MicropostTest < ActiveSupport::TestCase . . . test "order should be most recent first" do assert_equal microposts(:most_recent), Micropost.first endend

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

define some micropost fixtures

A

define some micropost fixtures

test/fixtures/microposts.ymlorange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %>tau_manifesto: content: "Check out the @tauday site by @mhartl: http://tauday.com" created_at: <%= 3.years.ago %>cat_video: content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk" created_at: <%= 2.hours.ago %>most_recent: content: "Writing a short test" created_at: <%= Time.zone.now %>

  • Note that we have explicitly set the created_at column using embedded Ruby. Because it’s a “magic” column automatically updated by Rails, setting it by hand isn’t ordinarily possible, but it is possible in fixtures.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Order microposts in descending order

A

Order microposts in descending order

app/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } validates :user_id, presence: true validates :content, presence: true, length: { maximum: 140 }end

  • To enforce a particular order, we’ll include the order argument in default_scope, which lets us order by the created_at column

order(:created_at)

  • Unfortunately, this orders the results in ascending order from smallest to biggest, which means that the oldest microposts come out first. To pull them out in reverse order, we can push down one level deeper and include a string with some raw SQL”

order('created_at DESC')

  • as of Rails 4.0 we can use a more natural pure-Ruby syntax”

order(created_at: :desc)

  • introduces the “stabby lambda” syntax for an object called a Proc (procedure) or lambda, which is an anonymous function (a function created without a name).
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

Pass an option to the has_many association method to destroy posts created by users that have been destroyed

A

Pass an option to the has_many association method to destroy posts created by users that have been destroyed

app/models/user.rbclass User < ApplicationRecord has_many :microposts, dependent: :destroy . . .end

  • Here the option dependent: :destroy arranges for the dependent microposts to be destroyed when the user itself is destroyed.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
17
Q

Write a test that saves a user (so it gets an id) and creates an associated micropost. Then check that destroying the user reduces the micropost count by 1.

A

Write a test that saves a user (so it gets an id) and creates an associated micropost. Then check that destroying the user reduces the micropost count by 1.

test/models/user_test.rbrequire 'test_helper'class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar") end . . . test "associated microposts should be destroyed" do @user.save @user.microposts.create!(content: "Lorem ipsum") assert_difference 'Micropost.count', -1 do @user.destroy end endend

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
18
Q

reset and reseed the database

A

reset and reseed the database

$ rails db:migrate:reset$ rails db:seed

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
19
Q

generate microposts controller

A

generate microposts controller

rails generate controller Microposts

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
20
Q

Create microposts partail

A

Create microposts partail

app/views/microposts/_micropost.html.erb≤li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <%= link_to micropost.user.name, micropost.user %>≤/span> <%= micropost.content %>≤/span> Posted <%= time_ago_in_words(micropost.created_at) %> ago. ≤/span>≤/li>

  • Note that we’ve used the ordered list tag ol (as opposed to an unordered list ul) because microposts are listed in a particular order (reverse-chronological).

≤ol class="microposts"> <%= render @microposts %>≤/ol>

  • This uses the awesome time_ago_in_words helper method
  • It also adds a CSS id for each micropost. This is a generally good practice, as it opens up the possibility of manipulating individual microposts at a future date (using JavaScript, for example).

≤li id="micropost-<%= micropost.id %>">

  • Note that we’ve used the ordered list tag ol (as opposed to an unordered list ul) because microposts are listed in a particular order (reverse-chronological).

≤ol class="microposts"> <%= render @microposts %>≤/ol>

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
21
Q

Add will paginate

A

Add will paginate

<%= will_paginate @microposts %>

  • before we got away with just saying “<%= will_paginate %>” because of the context it assumes users, but here the context is not microposts, so we must be specific. Of course, this means that we will have to define such a variable in the user show action.

app/controllers/users_controller.rbclass UsersController < ApplicationController . . . def show @user = User.find(params[:id]) @microposts = @user.microposts.paginate(page: params[:page]) end . . .end

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
22
Q

Our final task is to display the number of microposts for each user, which we can do with the count method:

A

Our final task is to display the number of microposts for each user, which we can do with the count method:

user.microposts.count

  • As with paginate, we can use the count method through the association. In particular, count does not pull all the microposts out of the database and then call length on the resulting array, as this would become inefficient as the number of microposts grew. Instead, it performs the calculation directly in the database, asking the database to count the microposts with the given user_id (an operation for which all databases are highly optimized).
  • (In the unlikely event that finding the count is still a bottleneck in your application, you can make it even faster using a counter cache.)
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
23
Q

Add microposts to the profile page

A

Add microposts to the profile page

app/views/users/show.html.erb<% provide(:title, @user.name) %>≤div class="row"> ≤aside class="col-md-4"> ≤section class="user_info"> ≤h1> <%= gravatar_for @user %> <%= @user.name %> ≤/h1> ≤/section> ≤/aside> ≤div class="col-md-8"> <% if @user.microposts.any? %> ≤h3>Microposts (<%= @user.microposts.count %>)≤/h3> ≤ol class="microposts"> <%= render @microposts %> ≤/ol> <%= will_paginate @microposts %> <% end %> ≤/div>≤/div>

  • Note the use of if @user.microposts.any?, which makes sure that an empty list won’t be displayed when the user has no microposts.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
24
Q
A
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
25
Q

Add microposts to sample data

A

Add microposts to sample data

db/seeds.rb...users = User.order(:created_at).take(6)50.times do content = Faker::Lorem.sentence(5) users.each { |user| user.microposts.create!(content: content) }end

  • Adding sample microposts for all the users actually takes a rather long time, so first we’ll select just the first six users (i.e., the five users with custom Gravatars, and one with the default Gravatar) using the take method:

User.order(:created_at).take(6)

  • For each of the selected users, we’ll make 50 microposts (plenty to overflow the pagination limit of 30). To generate sample content for each micropost, we’ll use the Faker gem’s handy Lorem.sentence method
  • (The reason for the order of the loops in this code is to intermix the microposts for use in the status feed. Looping over the users first gives feeds with big runs of microposts from the same user, which is visually unappealing.)
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
26
Q

reseed the development database

A

reseed the development database

$ rails db:migrate:reset$ rails db:seed

  • You should also quit and restart the Rails development server.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
27
Q

Create Micropost styling

A

Create Micropost styling

app/assets/stylesheets/custom.scss.../* microposts */.microposts { list-style: none; padding: 0; li { padding: 10px 0; border-top: 1px solid #e8e8e8; } .user { margin-top: 5em; padding-top: 0; } .content { display: block; margin-left: 60px; img { display: block; padding: 5px 0; } } .timestamp { color: $gray-light; display: block; margin-left: 60px; } .gravatar { float: left; margin-right: 10px; margin-top: 5px; }}aside { textarea { height: 100px; margin-bottom: 5px; }}span.picture { margin-top: 10px; input { border: 0; }}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
28
Q

Generate an integration test for the profiles of our site’s users

A

Generate an integration test for the profiles of our site’s users

rails generate integration_test users_profile

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
29
Q

Update Fixtures file

A

Update Fixtures file

test/fixtures/microposts.ymlorange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %> user: michaeltau_manifesto: content: "Check out the @tauday site by @mhartl: http://tauday.com" created_at: <%= 3.years.ago %> user: michaelcat_video: content: "Sad cats are sad: http://youtu.be/PKffm2uI4dk" created_at: <%= 2.hours.ago %> user: michaelmost_recent: content: "Writing a short test" created_at: <%= Time.zone.now %> user: michael<% 30.times do |n| %>micropost_<%= n %>: content: <%= Faker::Lorem.sentence(5) %> created_at: <%= 42.days.ago %> user: michael<% end %>

  • To test the micropost display on the profile, we need to associate the fixture microposts with a user. Rails includes a convenient way to build associations in fixtures, like this:

orange: content: "I just ate an orange!" created_at: <%= 10.minutes.ago %> user: michael

  • To test micropost pagination, we’ll also generate some additional micropost fixtures using the same embedded Ruby technique we used to make additional users

<% 30.times do |n| %>micropost_<%= n %>: content: <%= Faker::Lorem.sentence(5) %> created_at: <%= 42.days.ago %> user: michael<% end %>

30
Q

Write micropost test

A

Write micropost test

test/integration/users_profile_test.rbrequire 'test_helper'class UsersProfileTest < ActionDispatch::IntegrationTest include ApplicationHelper def setup @user = users(:michael) end test "profile display" do get user_path(@user) assert_template 'users/show' assert_select 'title', full_title(@user.name) assert_select 'h1', text: @user.name assert_select 'h1>img.gravatar' assert_match @user.microposts.count.to_s, response.body assert_select 'div.pagination' @user.microposts.paginate(page: 1).each do |micropost| assert_match micropost.content, response.body end endend

  • The test steps are: We visit the user profile page and check for the page title and the user’s name, Gravatar, micropost count, and paginated microposts.
  • Note the use of the full_title helper from the code to test the page’s title, which we gain access to by including the Application Helper module into the test.
  • The micropost count assertion in this code uses response.body which contains the full HTML source of the page (and not just the page’s body).
  • This means that if all we care about is that the number of microposts appears somewhere on the page, we can look for a match as follows:”

assert_match @user.microposts.count.to_s, response.body

  • This is a much less specific assertion than assert_select; in particular, unlike assert_select, using assert_match in this context doesn’t require us to indicate which HTML tag we’re looking for.
  • nesting syntax for assert_select:

assert_select 'h1>img.gravatar'

31
Q

Resources for microposts

A

Resources for microposts

config/routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] resources :microposts, only: [:create, :destroy]end

  • the interface to the Microposts resource will run principally through the Profile and Home pages, so we won’t need actions like new or edit in the Microposts controller; we’ll need only create and destroy.

HTTP request | URL | Action | Named routePOST | /microposts | create | microposts_pathDELETE | /microposts/1 | destroy | micropost_path(micropost)

32
Q

Write test to insure that users who aren’t logged in can’t edit or destroy microposts

A

Write test to insure that users who aren’t logged in can’t edit or destroy microposts

test/controllers/microposts_controller_test.rbrequire 'test_helper'class MicropostsControllerTest < ActionDispatch::IntegrationTest def setup @micropost = microposts(:orange) end test "should redirect create when not logged in" do assert_no_difference 'Micropost.count' do post microposts_path, params: { micropost: { content: "Lorem ipsum" } } end assert_redirected_to login_url end test "should redirect destroy when not logged in" do assert_no_difference 'Micropost.count' do delete micropost_path(@micropost) end assert_redirected_to login_url endend

  • Tests to enforce logged-in status mirror those for the Users controller. We simply issue the correct request to each action and confirm that the micropost count is unchanged and the result is redirected to the login URL.

because we access microposts through their associated users, both the create and destroy actions must require users to be logged in.” />

33
Q

Refactor the logged_in_user method into the application_controller so it can be used by mroe than just users

A

Refactor the logged_in_user method into the application_controller so it can be used by mroe than just users

app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper private # Confirms a logged-in user. def logged_in_user unless logged_in? store_location flash[:danger] = "Please log in." redirect_to login_url end endend

  • make sure you remove it from users_controller.rb
34
Q

Add create and destroy actions to microposts_controller.rb and then restrict access using the before filter

A

Add create and destroy actions to microposts_controller.rb and then restrict access using the before filter

app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create end def destroy endend

35
Q

Make the microposts create action

A

Make the microposts create action

app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create @micropost = current_user.microposts.build(micropost_params) if @micropost.save flash[:success] = "Micropost created!" redirect_to root_url else render 'static_pages/home' end end def destroy end private def micropost_params params.require(:micropost).permit(:content) endend

  • Use the user/micropost association to build the new micropost.
  • Note the use of strong parameters via micropost_params, which permits only the micropost’s content attribute to be modified through the web.
36
Q

Serve different versions of the Home page depending on a visitor’s login status.

A

Serve different versions of the Home page depending on a visitor’s login status.

app/views/static_pages/home.html.erb<% if logged_in? %> ≤div class="row"> ≤aside class="col-md-4"> ≤section class="user_info"> <%= render 'shared/user_info' %> ≤/section> ≤section class="micropost_form"> <%= render 'shared/micropost_form' %> ≤/section> ≤/aside> ≤/div><% else %> ≤div class="center jumbotron"> ≤h1>Welcome to the Sample App≤/h1> ≤h2> This is the home page for the ≤a href="http://www.railstutorial.org/">Ruby on Rails Tutorial≤/a> sample application. ≤/h2> <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %> ≤/div> <%= link_to image_tag("rails.png", alt: "Rails logo"), 'http://rubyonrails.org/' %><% end %>

37
Q

create and fill in a couple of partials. The first is the new Home page sidebar

A

create and fill in a couple of partials. The first is the new Home page sidebar

app/views/shared/_user_info.html.erb<%= link_to gravatar_for(current_user, size: 50), current_user %>≤h1><%= current_user.name %>≤/h1><%= link_to "view my profile", current_user %>≤/span><%= pluralize(current_user.microposts.count, "micropost") %>≤/span>

  • Note that, as in the profile sidebar, the user info in this code displays the total number of microposts for the user. There’s a slight difference in the display, though; in the profile sidebar, “Microposts” is a label, and showing “Microposts (1)” makes sense. In the present case, though, saying “1 microposts” is ungrammatical, so we arrange to display “1 micropost” and “2 microposts” using the pluralize method
38
Q

Define form for creating microposts

A

Define form for creating microposts

app/views/shared/_micropost_form.html.erb<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages', object: f.object %> ≤div class="field"> <%= f.text_area :content, placeholder: "Compose new micropost..." %> ≤/div> <%= f.submit "Post", class: "btn btn-primary" %><% end %>

39
Q

Add micropost instance variable to home action

A

Add micropost instance variable to home action

app/controllers/static_pages_controller.rbclass StaticPagesController < ApplicationController def home @micropost = current_user.microposts.build if logged_in? end def help end def about end def contact endend

  • current_user exists only if the user is logged in, so the @micropost variable should only be defined in this case.
40
Q

redefine the error-messages partial so the following code works:

A

redefine the error-messages partial so the following code works:

<%= render 'shared/error_messages', object: f.object %>

  • You may recall that the error-messages partial references the @user variable explicitly, but in the present case we have an @micropost variable instead. To unify these cases, we can pass the form variable f to the partial and access the associated object through f.object
  • This makes it so in “form_for(@user) do |f|” f.object is @user, and in “form_for(@micropost) do |f|” f.object is @micropost, etc.
41
Q

Error messages that work with other objects.

A

Error messages that work with other objects.

app/views/shared/_error_messages.html.erb<% if object.errors.any? %> ≤div id="error_explanation"> ≤div class="alert alert-danger"> The form contains <%= pluralize(object.errors.count, "error") %>. ≤/div> ≤ul> <% object.errors.full_messages.each do |msg| %> ≤li><%= msg %>≤/li> <% end %> ≤/ul> ≤/div><% end %>

  • To pass the object to the partial, we use a hash with value equal to the object and key equal to the desired name of the variable in the partial, which is what the second line in of the code accomplishes.
  • In other words, object: f.object creates a variable called object in the error_messages partial, and we can use it to construct a customized error message
42
Q

update the other occurrences of the error-messages partial, which we used when signing up users, resetting passwords, and editing users.

A

update the other occurrences of the error-messages partial, which we used when signing up users, resetting passwords, and editing users.

app/views/users/new.html.erb <%= render 'shared/error_messages', object: f.object %>app/views/users/edit.html.erb <%= render 'shared/error_messages', object: f.object %>app/views/password_resets/edit.html.erb <%= render 'shared/error_messages', object: f.object %>

43
Q

Since each user should have a feed, we are led naturally to a feed method in the User model, which will initially just select all the microposts belonging to the current user.

A

Since each user should have a feed, we are led naturally to a feed method in the User model, which will initially just select all the microposts belonging to the current user.

app/models/user.rbclass User < ApplicationRecord . . . # Defines a proto-feed. # See "Following users" for the full implementation. def feed Micropost.where("user_id = ?", id) end private . . .end

  • We’ll accomplish this using the where method on the Micropost model
  • Question mark in where method ensures that id is properly escaped before being included in the underlying SQL query, thereby avoiding a serious security hole called SQL injection. The id attribute here is just an integer (i.e., self.id, the unique ID of the user), so there is no danger of SQL injection in this case, but it does no harm, and always escaping variables injected into SQL statements is a good habit to cultivate.”

Micropost.where("user_id = ?", id)

  • Alert readers might note at this point that the code is essentially equivalent to writing”

def feed microposts end

  • But we used the other code instead because it generalizes much more naturally to the full status feed needed in Chapter 14.
44
Q

Use the feed in the sample application

A

Use the feed in the sample application

app/controllers/static_pages_controller.rbclass StaticPagesController < ApplicationController def home if logged_in? @micropost = current_user.microposts.build @feed_items = current_user.feed.paginate(page: params[:page]) end end def help end def about end def contact endend

  • we add an @feed_items instance variable for the current user’s (paginated) feed, and then add a status feed partial to the Home page.
  • Note that, now that there are two lines that need to be run when the user is logged in
  • change”

@micropost = current_user.microposts.build if logged_in?

  • to

@micropost = current_user.microposts.build

45
Q

Create status feed partail

A

Create status feed partail

app/views/shared/_feed.html.erb<% if @feed_items.any? %> ≤ol class="microposts"> <%= render @feed_items %> ≤/ol> <%= will_paginate @feed_items %><% end %>

  • The status feed partial defers the rendering to the micropost partial because here Rails knows to call the micropost partial because each element of @feed_items has class Micropost. This causes Rails to look for a partial with the corresponding name in the views directory of the given resource “app/views/microposts/_micropost.html.erb”

<%= render @feed_items %>

46
Q

Add the feed to the Home page by rendering the feed partial as usual

A

Add the feed to the Home page by rendering the feed partial as usual

app/views/static_pages/home.html.erb<% if logged_in? %> ≤div class="row"> ≤aside class="col-md-4"> ≤section class="user_info"> <%= render 'shared/user_info' %> ≤/section> ≤section class="micropost_form"> <%= render 'shared/micropost_form' %> ≤/section> ≤/aside> ≤div class="col-md-8"> ≤h3>Micropost Feed≤/h3> <%= render 'shared/feed' %> ≤/div> ≤/div><% else %> . . .<% end %>

47
Q

create failed micropost submission workaround so it doesn’t break the feed

A

create failed micropost submission workaround so it doesn’t break the feed

app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] def create @micropost = current_user.microposts.build(micropost_params) if @micropost.save flash[:success] = "Micropost created!" redirect_to root_url else @feed_items = [] render 'static_pages/home' end end

  • on failed micropost submission, the Home page expects an @feed_items instance variable, so failed submissions currently break. The easiest solution is to suppress the feed entirely by assigning it an empty array, as shown in Listing 13.50.(Unfortunately, returning a paginated feed doesn’t work in this case. Implement it and click on a pagination link to see why.)
48
Q

add a delete link to the micropost partial

A

add a delete link to the micropost partial

app/views/microposts/_micropost.html.erb≤li id="<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <%= link_to micropost.user.name, micropost.user %>≤/span> <%= micropost.content %>≤/span> Posted <%= time_ago_in_words(micropost.created_at) %> ago. <% if current_user?(micropost.user) %> <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %> <% end %> ≤/span>≤/li>

49
Q

define a destroy action in the Microposts controller

A

define a destroy action in the Microposts controller

app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] before_action :correct_user, only: :destroy . . . def destroy @micropost.destroy flash[:success] = "Micropost deleted" redirect_to request.referrer || root_url end private def micropost_params params.require(:micropost).permit(:content) end def correct_user @micropost = current_user.microposts.find_by(id: params[:id]) redirect_to root_url if @micropost.nil? endend

  • rather than using an @uservariable with an admin_user before filter, we’ll find the micropost through the association, which will automatically fail if a user tries to delete another user’s micropost.
  • We’ll put the resulting find inside a correct_user before filter, which checks that the current user actually has a micropost with the given id.
50
Q

request.referrer method

A

request.referrer method

request.referrer || root_url

  • related to the request.original_url variable used in friendly forwarding
  • is just the previous URL (in this case, the Home page).
  • This is convenient because microposts appear on both the Home page and on the user’s profile page, so by using request.referrer we arrange to redirect back to the page issuing the delete request in both cases.
  • If the referring URL is nil (as is the case inside some tests), the code sets the root_url as the default using the || operator.
51
Q

start by adding a few microposts with different owners to the micropost fixtures

A

start by adding a few microposts with different owners to the micropost fixtures

test/fixtures/microposts.yml...ants: content: "Oh, is that what you want? Because that's how you get ants!" created_at: <%= 2.years.ago %> user: archerzone: content: "Danger zone!" created_at: <%= 3.days.ago %> user: archertone: content: "I'm sorry. Your words made sense, but your sarcastic tone did not." created_at: <%= 10.minutes.ago %> user: lanavan: content: "Dude, this van's, like, rolling probable cause." created_at: <%= 4.hours.ago %> user: lana

  • We’ll be using only one for now, but we’ve put in the others for future reference.
52
Q

Add microposts with different owners

A

Add microposts with different owners

test/fixtures/microposts.yml...ants: content: "Oh, is that what you want? Because that's how you get ants!" created_at: <%= 2.years.ago %> user: archerzone: content: "Danger zone!" created_at: <%= 3.days.ago %> user: archertone: content: "I'm sorry. Your words made sense, but your sarcastic tone did not." created_at: <%= 10.minutes.ago %> user: lanavan: content: "Dude, this van's, like, rolling probable cause." created_at: <%= 4.hours.ago %> user: lana

53
Q

Write a short test to make sure one user can’t delete the microposts of a different user, and we also check for the proper redirect

A

Write a short test to make sure one user can’t delete the microposts of a different user, and we also check for the proper redirect

test/controllers/microposts_controller_test.rbrequire 'test_helper'... test "should redirect destroy for wrong micropost" do log_in_as(users(:michael)) micropost = microposts(:ants) assert_no_difference 'Micropost.count' do delete micropost_path(micropost) end assert_redirected_to root_url endend

54
Q

generate micropost interface test

A

generate micropost interface test

rails generate integration_test microposts_interface

55
Q

Write the interface test

A

Write the interface test

test/integration/microposts_interface_test.rbrequire 'test_helper'class MicropostsInterfaceTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "micropost interface" do log_in_as(@user) get root_path assert_select 'div.pagination' # Invalid submission assert_no_difference 'Micropost.count' do post microposts_path, params: { micropost: { content: "" } } end assert_select 'div#error_explanation' # Valid submission content = "This micropost really ties the room together" assert_difference 'Micropost.count', 1 do post microposts_path, params: { micropost: { content: content } } end assert_redirected_to root_url follow_redirect! assert_match content, response.body # Delete post assert_select 'a', text: 'delete' first_micropost = @user.microposts.paginate(page: 1).first assert_difference 'Micropost.count', -1 do delete micropost_path(first_micropost) end # Visit different user (no delete links) get user_path(users(:archer)) assert_select 'a', text: 'delete', count: 0 endend

  • write an integration test to log in, check the micropost pagination, make an invalid submission, make a valid submission, delete a post, and then visit a second user’s page to make sure there are no “delete” links.
56
Q

Include CarrierWave image uploader and mini_magick gems in the Gemfile

A

Include CarrierWave image uploader and mini_magick gems in the Gemfile

source 'https://rubygems.org'gem 'rails', '5.1.6'gem 'bcrypt', '3.1.12'gem 'faker', '1.7.3'gem 'carrierwave', '1.2.2'gem 'mini_magick', '4.7.0'gem 'will_paginate', '3.1.5'gem 'bootstrap-will_paginate', '1.0.0'...group :production do gem 'pg', '0.20.0' gem 'fog', '1.42'end...

  • also include the fog gem for image upload in production

bundle install

  • CarrierWave adds a Rails generator for creating an image uploader, which we’ll use to make an uploader for an image called picture”

rails generate uploader Picture

  • Images uploaded with CarrierWave should be associated with a corresponding attribute in an Active Record model, which simply contains the name of the image file in a string field.
57
Q

add the required picture attribute to the Micropost model

A

add the required picture attribute to the Micropost model

$ rails generate migration add_picture_to_microposts picture:string$ rails db:migrate

58
Q

tell CarrierWave to associate the image with a model by using the mount_uploader method

A

tell CarrierWave to associate the image with a model by using the mount_uploader method

mount_uploader :picture, PictureUploader

  • takes as arguments a symbol representing the attribute and the class name of the generated uploader:
59
Q

Add association to micropost.rb

A

Add association to micropost.rb

app/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } mount_uploader :picture, PictureUploader validates :user_id, presence: true validates :content, presence: true, length: { maximum: 140 }end

60
Q

include a file_field tag in the micropost form

A

include a file_field tag in the micropost form

app/views/shared/_micropost_form.html.erb<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages', object: f.object %> ≤div class="field"> <%= f.text_area :content, placeholder: "Compose new micropost..." %> ≤/div> <%= f.submit "Post", class: "btn btn-primary" %> <%= f.file_field :picture %> ≤/span><% end %>

61
Q

add picture to the list of attributes permitted to be modified through the web.

A

add picture to the list of attributes permitted to be modified through the web.

app/controllers/microposts_controller.rbclass MicropostsController < ApplicationController before_action :logged_in_user, only: [:create, :destroy] before_action :correct_user, only: :destroy . . . private def micropost_params params.require(:micropost).permit(:content, :picture) end def correct_user @micropost = current_user.microposts.find_by(id: params[:id]) redirect_to root_url if @micropost.nil? endend

  • edit the micropost_params method
62
Q

render it using the image_tag helper in the micropost partial

A

render it using the image_tag helper in the micropost partial

app/views/microposts/_micropost.html.erb≤li id="micropost-<%= micropost.id %>"> <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %> <%= link_to micropost.user.name, micropost.user %>≤/span> <%= micropost.content %> <%= image_tag micropost.picture.url if micropost.picture? %> ≤/span> Posted <%= time_ago_in_words(micropost.created_at) %> ago. <% if current_user?(micropost.user) %> <%= link_to "delete", micropost, method: :delete, data: { confirm: "You sure?" } %> <% end %> ≤/span>≤/li>

  • Notice the use of the picture? boolean method to prevent displaying an image tag when there isn’t an image. This method is created automatically by CarrierWave based on the name of the image attribute.
63
Q

uncomment out the whitelist restriction in picture_uploader.rb

A

uncomment out the whitelist restriction in picture_uploader.rb

app/uploaders/picture_uploader.rbclass PictureUploader < CarrierWave::Uploader::Base storage :file # Override the directory where uploaded files will be stored. # This is a sensible default for uploaders that are meant to be mounted: def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # Add a white list of extensions which are allowed to be uploaded. def extension_whitelist %w(jpg jpeg gif png) endend

64
Q

define a custom validation called picture_size

A

define a custom validation called picture_size

app/models/micropost.rbclass Micropost < ApplicationRecord belongs_to :user default_scope -> { order(created_at: :desc) } mount_uploader :picture, PictureUploader validates :user_id, presence: true validates :content, presence: true, length: { maximum: 140 } validate :picture_size private # Validates the size of an uploaded picture. def picture_size if picture.size > 5.megabytes errors.add(:picture, "should be less than 5MB") end endend

  • In contrast to previous model validations, file size validation doesn’t correspond to a built-in Rails validator. As a result, validating images requires defining a custom validation
  • Note the use of validate (as opposed to validates) to call a custom validation.
65
Q

Create two client side validations for image uploads

A

Create two client side validations for image uploads

app/views/shared/_micropost_form.html.erb<%= form_for(@micropost) do |f| %> <%= render 'shared/error_messages', object: f.object %> ≤div class="field"> <%= f.text_area :content, placeholder: "Compose new micropost..." %> ≤/div> <%= f.submit "Post", class: "btn btn-primary" %> <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %> ≤/span><% end %>≤script type="text/javascript"> $('#micropost_picture').bind('change', function() { var size_in_megabytes = this.files[0].size/1024/1024; if (size_in_megabytes > 5) { alert('Maximum file size is 5MB. Please choose a smaller file.'); } });≤/script>

  • use the acceptparameter in the file_field input tag

<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>

  • include a little JavaScript (or, more specifically, jQuery) to issue an alert if a user tries to upload an image that’s too big (which prevents accidental time-consuming uploads and lightens the load on the server):”

$('#micropost_picture').bind('change', function() { var size_in_megabytes = this.files[0].size/1024/1024; if (size_in_megabytes > 5) { alert('Maximum file size is 5MB. Please choose a smaller file.'); } });

  • the code above monitors the page element containing the CSS id micropost_picture (as indicated by the hash mark #), which is the id of the micropost form in the code.
  • (The way to figure this out is to Ctrl-click and use your browser’s web inspector.) When the element with that CSS id changes, the jQuery function fires and issues the alert method if the file is too big.19
  • there would be no way to prevent a user from editing the JavaScript with a web inspector or issue a direct POST request using, e.g., curl. To prevent users from uploading arbitrarily large files, it is thus essential to include a server-side validation
66
Q

Install ImageMagick on the development environment.

A

Install ImageMagick on the development environment.

brew install imagemagick

67
Q

Include CarrierWave’s MiniMagick interface for ImageMagick, together with a resizing command.

A

Include CarrierWave’s MiniMagick interface for ImageMagick, together with a resizing command.

app/uploaders/picture_uploader.rbclass PictureUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick process resize_to_limit: [400, 400] storage :file # Override the directory where uploaded files will be stored. # This is a sensible default for uploaders that are meant to be mounted: def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # Add a white list of extensions which are allowed to be uploaded. def extension_whitelist %w(jpg jpeg gif png) endend

  • For the resizing command, there are several possibilities listed in the MiniMagick documentation, but the one we want is “resize_to_limit: [400, 400]”, which resizes large images so that they aren’t any bigger than 400px in either dimension, while simultaneously leaving smaller images alone.
68
Q

Use a cloud storage service to store images separately from our application by using the fog gem.

A

Use a cloud storage service to store images separately from our application by using the fog gem.

app/uploaders/picture_uploader.rbclass PictureUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick process resize_to_limit: [400, 400] if Rails.env.production? storage :fog else storage :file end # Override the directory where uploaded files will be stored. # This is a sensible default for uploaders that are meant to be mounted: def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end # Add a white list of extensions which are allowed to be uploaded. def extension_whitelist %w(jpg jpeg gif png) endend

  • uses the production? boolean to switch storage method based on the environment
69
Q

use one of the most popular and well-supported, Amazon.com’s Simple Storage Service (S3).

A

use one of the most popular and well-supported, Amazon.com’s Simple Storage Service (S3).

  • Sign up for an Amazon Web Services account if you don’t have one already. If you signed up for the Cloud9 IDE in Section 1.2.1, you already have an AWS account and can skip this step.
  • Create a user via AWS Identity and Access Management (IAM) and record the access key and secret key.
  • Create an S3 bucket (with a name of your choice) using the AWS Console, and then grant read and write permission to the user created in the previous step.
70
Q

create and fill the CarrierWave configuration file

A

create and fill the CarrierWave configuration file

config/initializers/carrier_wave.rbif Rails.env.production? CarrierWave.configure do |config| config.fog_credentials = { # Configuration for Amazon S3 :provider => 'AWS', :aws_access_key_id => ENV['S3_ACCESS_KEY'], :aws_secret_access_key => ENV['S3_SECRET_KEY'] } config.fog_directory = ENV['S3_BUCKET'] endend

  • Note: If your setup isn’t working, your region location may be the issue. Some users may have to add :region => ENV[‘S3_REGION’] to the fog credentials, followed by heroku config:set S3_REGION=<bucket region> at the command line, where the bucket region should be something like ‘eu-central-1’, depending on your location. To determine the correct region, consult the list of Regions and Endpoints at Amazon.
  • As with production email configuration, this code uses Heroku ENVvariables to avoid hard-coding sensitive information. Previously these variables were defined automatically via the SendGrid add-on, but in this case we need to define them explicitly, which we can accomplish using heroku config:set as follows:”

$ heroku config:set S3_ACCESS_KEY=≤access key> $ heroku config:set S3_SECRET_KEY=≤secret key> $ heroku config:set S3_BUCKET=

  • Update .gitignore file so that the image uploads directory is ignored.

Listing 13.71: Adding the uploads directory to the .gitignore file....# Ignore uploaded test images./public/uploads

71
Q

Deploy to Heroku

A

Deploy to Heroku

$ rails test$ git add -A$ git commit -m "Add user microposts"$ git checkout master$ git merge user-microposts$ git push$ git push heroku$ heroku pg:reset DATABASE$ heroku run rails db:migrate$ heroku run rails db:seed

72
Q

What we learned in chapter 13

A

What we learned in this chapter

  • Microposts, like Users, are modeled as a resource backed by an Active Record model.
  • Rails supports multiple-key indices.
  • We can model a user having many microposts using the has_many and belongs_tomethods in the User and Micropost models, respectively.
  • The has_many/belongs_to combination gives rise to methods that work through the association.
  • The code user.microposts.build(…) returns a new Micropost object automatically associated with the given user.
  • Rails supports default ordering via default_scope.
  • Scopes take anonymous functions as arguments.
  • The dependent: :destroy option causes objects to be destroyed at the same time as associated objects.
  • Pagination and object counts can both be performed through associations, leading to automatically efficient code.
  • Fixtures support the creation of associations.
  • It is possible to pass variables to Rails partials.
  • The where method can be used to perform Active Record selections.
  • We can enforce secure operations by always creating and destroying dependent objects through their association.
  • We can upload and resize images using CarrierWave.