Ch 13: User microposts Flashcards
Create topic branch
Create topic branch
git checkout -b user-microposts
generate the Micropost model
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
Add index to several columns
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.
Write test for microposts
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
validate for presence of user ID for microposts
validate for the presence of user ID for microposts
app/models/micropost.rbclass Micropost < ActiveRecord::Base belongs_to :user validates :user_id, presence: trueend
Add tests for validations
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:
Add validations for the micropost’s content attribute
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
replace the idiomatically incorrect code
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.
Using the belongs_to/has_many association defined in this section, Rails constructs these methods
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.” />
Association methods
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
update the User and Micropost models with code to associate them together
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
update the setup method with the idiomatically correct way to build a new micropost
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
write a test to verify that the first micropost in the database is the same as a fixture micropost we’ll call most_recent
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
define some micropost fixtures
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.
Order microposts in descending order
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).
Pass an option to the has_many association method to destroy posts created by users that have been destroyed
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.
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.
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
reset and reseed the database
reset and reseed the database
$ rails db:migrate:reset$ rails db:seed
generate microposts controller
generate microposts controller
rails generate controller Microposts
Create microposts partail
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>
Add will paginate
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
Our final task is to display the number of microposts for each user, which we can do with the count method:
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.)
Add microposts to the profile page
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.
Add microposts to sample data
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.)
reseed the development database
reseed the development database
$ rails db:migrate:reset$ rails db:seed
- You should also quit and restart the Rails development server.
Create Micropost styling
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; }}
Generate an integration test for the profiles of our site’s users
Generate an integration test for the profiles of our site’s users
rails generate integration_test users_profile
Update Fixtures file
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 %>
Write micropost test
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'
Resources for microposts
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)
Write test to insure that users who aren’t logged in can’t edit or destroy microposts
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.” />
Refactor the logged_in_user method into the application_controller so it can be used by mroe than just users
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
Add create and destroy actions to microposts_controller.rb and then restrict access using the before filter
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
Make the microposts create action
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.
Serve different versions of the Home page depending on a visitor’s login status.
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 %>
create and fill in a couple of partials. The first is the new Home page sidebar
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
Define form for creating microposts
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 %>
Add micropost instance variable to home action
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.
redefine the error-messages partial so the following code works:
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.
Error messages that work with other objects.
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
update the other occurrences of the error-messages partial, which we used when signing up users, resetting passwords, and editing users.
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 %>
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.
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.
Use the feed in the sample application
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
Create status feed partail
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 %>
Add the feed to the Home page by rendering the feed partial as usual
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 %>
create failed micropost submission workaround so it doesn’t break the feed
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.)
add a delete link to the micropost partial
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>
define a destroy action in the Microposts controller
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.
request.referrer method
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.
start by adding a few microposts with different owners to the micropost fixtures
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.
Add microposts with different owners
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
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
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
generate micropost interface test
generate micropost interface test
rails generate integration_test microposts_interface
Write the interface test
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.
Include CarrierWave image uploader and mini_magick gems in the Gemfile
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.
add the required picture attribute to the Micropost model
add the required picture attribute to the Micropost model
$ rails generate migration add_picture_to_microposts picture:string$ rails db:migrate
tell CarrierWave to associate the image with a model by using the mount_uploader method
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:
Add association to micropost.rb
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
include a file_field tag in the micropost form
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 %>
add picture to the list of attributes permitted to be modified through the web.
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
render it using the image_tag helper in the micropost partial
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.
uncomment out the whitelist restriction in picture_uploader.rb
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
define a custom validation called picture_size
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.
Create two client side validations for image uploads
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
Install ImageMagick on the development environment.
Install ImageMagick on the development environment.
brew install imagemagick
Include CarrierWave’s MiniMagick interface for ImageMagick, together with a resizing command.
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.
Use a cloud storage service to store images separately from our application by using the fog gem.
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
use one of the most popular and well-supported, Amazon.com’s Simple Storage Service (S3).
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.
create and fill the CarrierWave configuration file
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
Deploy to Heroku
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
What we learned in chapter 13
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.