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