Rails 教程:nil:NilClass 的未定义方法

Rails Tutorial: undefined method for nil:NilClass

我成功完成了 Michael Hartl 的 Rails 教程,您在其中构建了一个类似 Twitter 的应用程序。现在我想补充一些其他的东西,但在尝试为微博添加类似功能时卡住了。

作为基础,我采用了关注者之间的关系。

所以一个用户has_many(微博)'liked'和一个微博has_many'likers'(用户)

我一直收到错误消息

    ActionView::Template::Error: undefined method 'likers' for nil:NilClass

在微博界面测试和用户配置文件测试中。

这是我的代码:

routes.rb:

Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
    resources :users do
      member do
        get :following, :followers, :liked
      end
    end


    resources :microposts do
      member do
        get :likers
      end
    end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
end

app/models/like.rb:

class Like < ActiveRecord::Base
  belongs_to :liker, class_name: "User"
  belongs_to :micropost, class_name: "Micropost"
  validates :liker_id, presence: true
  validates :micropost_id, presence: true
end

app/models/micropost.rb:

class Micropost < ActiveRecord::Base
  belongs_to :user
  has_many :passive_likes, class_name:  "Like",
                           foreign_key: "micropost_id",
                           dependent:   :destroy
  has_many :likers, through: :passive_likes, source: :micropost
  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, "maximal 5MB")
      end
    end
end

app/models/user.rb:

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :active_likes, class_name:  "Like",
                          foreign_key: "liker_id",
                          dependent:   :destroy
  has_many :liked, through: :active_likes, source: :micropost
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower

  mount_uploader :avatar, AvatarUploader

  attr_accessor :remember_token, :activation_token, :reset_token
  before_save   :downcase_email
  before_create :create_activation_digest
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, length: { minimum: 6 }, allow_blank: true

  # Returns the hash digest of the given string.
  def self.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def self.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # Returns true if the given token matches the digest.
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

  # Forgets a user.
  def forget
    update_attribute(:remember_digest, nil)
  end

   # Activates an account.
  def activate
    update_attribute(:activated,    true)
    update_attribute(:activated_at, Time.zone.now)
  end

  # Sends activation email.
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

  # Sets the password reset attributes.
  def create_reset_digest
    self.reset_token = User.new_token
    update_attribute(:reset_digest,  User.digest(reset_token))
    update_attribute(:reset_sent_at, Time.zone.now)
  end

  # Sends password reset email.
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

  # Returns true if a password reset has expired.
  def password_reset_expired?
    reset_sent_at < 2.hours.ago
  end

  # Defines a proto-feed.
  # See "Following users" for the full implementation.
  # Returns a user's status feed.
  def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE  follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
  end

  # Follows a user.
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end

  # Unfollows a user.
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # Returns true if the current user is following the other user.
  def following?(other_user)
    following.include?(other_user)
  end

  # Likes a micropost
  def like(any_post)
    active_like.create(micropost_id: any_post.id)
  end

  # Unlikes a micropost
  def unlike(any_post)
    active_like.find_by(micropost_id: any_post.id).destroy
  end

  # Returns true if the current user is liking the micropost
  def liked?(any_post)
    liked.include?(any_post)
  end

  private

    # Converts email to all lower-case.
    def downcase_email
      self.email = email.downcase
    end

    # Creates and assigns the activation token and digest.
    def create_activation_digest
      self.activation_token  = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

app/views/users/show.html.erb:

<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section>
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
    <section class="stats">
      <%= render 'shared/stats' %>
    </section>
  </aside>
  <div class="col-md-8">
    <%= render 'follow_form' if logged_in? %>
    <% if @user.microposts.any? %>
      <h3>Posts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>

app/views/microposts/_micropost.html.erb:

<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <%= render 'shared/morestats' %>
      <%= render 'microposts/like_form' if logged_in? %>
    <% if current_user?(micropost.user) %>

      <%= link_to "löschen", micropost, method: :delete,
                                       data: { confirm: "Diesen Post wirklich löschen?" } %>
    <% end %>
  </span>
</li>

app/views/microposts/_like_form.html.erb:

<% unless current_user?(@user) %>
  <div id="like_form">
  <% if @micropost.liked?(@user) %>
    <%= render 'unlike' %>
  <% else %>
    <%= render 'like' %>
  <% end %>
  </div>
<% end %>

app/views/microposts/_like.html.erb:

<%= form_for(current_user.active_likes.build) do |f| %>
  <div><%= hidden_field_tag :micropost_id, @user.id %></div>
  <%= f.submit "Like", class: "btn btn-primary" %>
<% end %>

app/views/microposts/_unlike.html.erb:

<%= form_for(current_user.active_likes.build) do |f| %>
  <div><%= hidden_field_tag :micropost_id, @user.id %></div>
  <%= f.submit "Like", class: "btn btn-primary" %>
<% end %>

如果缺少任何代码,请告诉我。提前致谢!


这是完整的错误:

ERROR["test_micropost_interface", MicropostsInterfaceTest, 4.72409967]
 test_micropost_interface#MicropostsInterfaceTest (4.72s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `likers' for nil:NilClass
            app/views/shared/_morestats.html.erb:4:in `_app_views_shared__morestats_html_erb___939926434685355917_93681000'
            app/views/microposts/_micropost.html.erb:10:in `_app_views_microposts__micropost_html_erb___1029196025817541101_93766560'
            app/views/users/show.html.erb:19:in `_app_views_users_show_html_erb___2389533090581269630_85562520'
            test/integration/microposts_interface_test.rb:33:in `block in <class:MicropostsInterfaceTest>'
        app/views/shared/_morestats.html.erb:4:in `_app_views_shared__morestats_html_erb___939926434685355917_93681000'
        app/views/microposts/_micropost.html.erb:10:in `_app_views_microposts__micropost_html_erb___1029196025817541101_93766560'
        app/views/users/show.html.erb:19:in `_app_views_users_show_html_erb___2389533090581269630_85562520'
        test/integration/microposts_interface_test.rb:33:in `block in <class:MicropostsInterfaceTest>'

代码 test/integration/microposts_interface_test.rb:

require 'test_helper'

class MicropostsInterfaceTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:peter)
  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, 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, micropost: { content: content }
    end
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    # Delete a post.
    assert_select 'a', text: 'löschen'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # Visit a different user.
    get user_path(users(:archer))
    assert_select 'a', text: 'löschen', count: 0
  end
end

代码 test/integration/users_profile_test.rb:

require 'test_helper'

class UsersProfileTest < ActionDispatch::IntegrationTest
  include ApplicationHelper

  def setup
    @user = users(:peter)
  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
  end
end

代码 shared/_morestats.html.erb:

<% @micropost %>
<div class="morestats">
  <strong id="likers" class="morestat">
    <%= @micropost.likers.count %>
  </strong>
  likers
</div>

问题是您编写的@micropost 引用了一个对所有视图可见的变量(您从未分配过的@micropost 对象)。如果你写的微博没有@,你指的是一个用 :locals => { :micropost => micropost } 赋值的局部变量。

在shared/morestats的第4行,你做的是@micropost.likers,但是你注意的话,你并没有把任何微博传递给部分(见微博,第10行):

<%= render 'shared/morestats' %>

你必须把它改成这样:

<%= render :partial => 'shared/morestats', :locals => { :micropost => micropost } %>

并在更多统计中删除微博中的“@”。就像这样:

<% micropost %>
<div class="morestats">
  <strong id="likers" class="morestat">
    <%= micropost.likers.count %>
  </strong>
  likers
</div>

您在这里遇到了同样的问题:

<%= render 'microposts/like_form' %>

你必须把它改成这样:

<%= render :partial => 'microposts/like_form', :locals => { :micropost => micropost } %>

和like_form到这个:

<% unless current_user?(@user) %>
  <div id="like_form">
  <% if @user.liked?(micropost) %>
    <%= render 'unlike' %>
  <% else %>
    <%= render 'like' %>
  <% end %>
  </div>
<% end %>

另一个解决方案,如果你不想改变@,你可以像这样修改_micropost部分:

<% @micropost = micropost %>
<%= render 'shared/morestats' %>

但这不如以前的解决方案优雅。

      <%= @book.likes.count %>
        <% like = current_user.likes.find_by(book: @book) %>
        <%# if like.nil? %>
           <%= button_to "like", likes_path, params: {like: {book_id: @book.id }}, method: :post %>
        <%# else %>
           <%#= button_to "UnLike", like_path(like), method: :delete %>
   <% #end %>
  • -->