Учебник по Rails: неопределенный метод для nil:NilClass

Я успешно закончил учебник по Rails от Майкла Хартла, в котором вы создаете приложение, такое как Twitter. Теперь я хочу добавить еще кое-что и застрял, пытаясь добавить лайк-функцию к микропостам.

За основу для этого я взял отношения между подписчиками.

Таким образом, пользователь has_many (микросообщения) «понравилось», а микросообщение has_many «лайкеров» (пользователей).

Я продолжаю получать ошибку

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

в тесте интерфейса микросообщений и в тесте профиля пользователя.

Вот мой код:

маршруты.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

приложение/модели/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

приложение/модели/микропост.рб:

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

приложение/модели/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

приложение/просмотры/пользователи/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

код для теста/интеграции/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>

person Nicolas Scherer    schedule 26.02.2015    source источник
comment
Куда указывает ошибка?   -  person Marek Lipka    schedule 26.02.2015
comment
Я добавил полную ошибку в конец моего вопроса   -  person Nicolas Scherer    schedule 26.02.2015
comment
Не могли бы вы также добавить код для тестов?   -  person Magnus    schedule 26.02.2015


Ответы (1)


Проблема в том, что вы написали @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' %>

Но это менее элегантно, чем предыдущее решение.

person eritiro    schedule 26.02.2015
comment
Спасибо за Ваш ответ! Это имеет смысл, но я все еще получаю те же сообщения об ошибках. - person Nicolas Scherer; 26.02.2015
comment
Я добавил код для shared/_morestats.html.erb в свой вопрос. Это помогает? Я действительно не знаю, что еще я мог бы попробовать. - person Nicolas Scherer; 26.02.2015
comment
Я уже пробовал это, но исчезло только одно сообщение об ошибке. ошибка для MicropostsInterfaceTest остается прежней - person Nicolas Scherer; 26.02.2015
comment
Ошибка теперь неопределенный метод 'понравилось?' для nil: Nilclass в microposts/_like_form.html.erb строка 3 - person Nicolas Scherer; 26.02.2015
comment
Снова та же проблема: добавьте локальную переменную в частичный вызов Like_form и удалите @ из микросообщений. И учесть, что понравилось? это пользовательский метод, а не метод микросообщения. - person eritiro; 26.02.2015
comment
Я не очень понимаю, что вы имеете в виду. Не могли бы вы объяснить мне, какие части моего кода я должен изменить или удалить? Извините, если это кажется вам очевидным, но я не очень разбираюсь в рельсах. - person Nicolas Scherer; 26.02.2015
comment
да. не волнуйся. Дайте мне секунду, чтобы обновить мой ответ. - person eritiro; 26.02.2015
comment
спасибо, теперь ошибка исчезла. но теперь есть синтаксическая ошибка: «неожиданное ключевое слово_ensure, ожидание конца ввода» в строке 10 файла _like_form.html. этот частичный не имеет даже 10 строк. Что я делаю неправильно? - person Nicolas Scherer; 26.02.2015
comment
добавь меня в скайп: emiliano.ritiro - person eritiro; 26.02.2015