mardi 7 juillet 2015

I get error: First argument in form can't contain nil or empty, even though it doesn't contain nil or empty

I've finished chapter 10 of Michael Hartl's Rails Tutorial, and I'm trying to add the password reset ability (part of chapter 10) to a new app that I'm making. But when I rake, I get an error:

ERROR["test_password_resets", PasswordResetsTest, 2015-07-07 15:21:45 -0700]
 test_password_resets#PasswordResetsTest (1436307705.90s)
ActionView::Template::Error:         ActionView::Template::Error: First argument in form cannot contain nil or be empty
            app/views/password_resets/edit.html.erb:8:in `_app_views_password_resets_edit_html_erb___1321587905929812469_70306702568520'
            test/integration/password_resets_test.rb:25:in `block in <class:PasswordResetsTest>'
        app/views/password_resets/edit.html.erb:8:in `_app_views_password_resets_edit_html_erb___1321587905929812469_70306702568520'
        test/integration/password_resets_test.rb:25:in `block in <class:PasswordResetsTest>'

Here is the edit.html.erb (where I'm having the problem):

<% provide(:title, "Reset your password") %>
<div class = "center jumbotron white">
    <h2>Reset Your Password</h2>
    <p class = "center">Choose your new password. Be wise about it and don't choose something you'll forget.  </p>

    <div class="row">
      <div class="col-md-6 col-md-offset-3 top-medium-margins">
        <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %>
            <%= render 'shared/error_messages' %>

            <%= hidden_field_tag :email, @user.email %>

            <%= f.password_field :password, class: 'form-control', placeholder: 'New password' %>

            <%= f.password_field :password_confirmation, class: 'form-control', placeholder: 'Confirm your new password' %>

            <%= f.submit "Update Password", class: "center btn btn-default green-hover top-small-margins" %>
        <% end %>
      </div>
    </div>
</div>

Here is my password resets controller:

class PasswordResetsController < ApplicationController
  before_action :get_user,         only: [:edit, :update]
  before_action :valid_user,       only: [:edit, :update]
  before_action :check_expiration, only: [:edit, :update]
  before_action :logged_in_change_links, only: [:new, :edit, :update]
  def new
  end

  def create
    @user = User.find_by(email: params[:password_reset][:email].downcase)
    if @user
      @user.create_reset_digest
      @user.send_password_reset_email
        flash[:info] = "An email was sent to " + @user.email + " with password reset instructions."
        redirect_to login_url
    else
        flash.now[:danger] = "Hmm. We don't recognize that email. Make sure you signed up with this email."
        render 'new'
    end
  end

  def edit
  end

  def update
    if params[:user][:password].empty?
      flash.now[:danger] = "Uh oh. Your password can't be empty."
      render 'edit'
    elsif @user.update_attributes(user_params)
      log_in @user
      flash[:success] = "Success! Your password has been reset!"
      redirect_to @user
    else
      render 'edit'
    end
  end

  private

  def user_params
    params.require(:user).permit(:password, :password_confirmation)
  end

  #BEFORE FILTERS

  #Forgot => Profile if logged in
  def logged_in_change_links
    unless not logged_in?
      redirect_to current_user
    end
  end

  def get_user
    @user = User.find_by(email: params[:email])
    puts "Got user!"
  end

  # Confirms a valid user.
  def valid_user
    if (@user && @user.authenticated?(:reset, params[:id]))
      redirect_to root_url
      puts("Either I don't exist or I'm not authenticated.")
    end
  end

  # Checks expiration of reset token.
  def check_expiration
    if (@user && @user.password_reset_expired?)
      flash[:danger] = "Uh oh! Your password reset has expired. Please request a new password reset."
      redirect_to new_password_reset_url
    end
  end

end

And my user model:

class User < ActiveRecord::Base
    attr_accessor :remember_token, :reset_token
    before_save { email.downcase! }


    validates :name, presence: true, length: { maximum: 50 }

    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i #Set valid email regex to be used
    validates :email, presence: true, length: { maximum: 255 }, format: {with: VALID_EMAIL_REGEX}, uniqueness: { case_sensitive: false}

    has_secure_password
    validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

    # Returns the hash digest of the given string.
    def User.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 User.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

    #PASSWORD RESET
  def create_reset_digest
    "I'm at the beginning of 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)
    puts "I got to the end of create_reset_digest"
  end

  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
end

And finally, the actual integration test that tests the password reset: require 'test_helper'

class PasswordResetsTest < ActionDispatch::IntegrationTest
  def setup
    ActionMailer::Base.deliveries.clear
    @user = users(:michael)
  end

  test "password resets" do
    get new_password_reset_path
    assert_template 'password_resets/new'
    # Invalid email
    post password_resets_path, password_reset: { email: "" }
    assert_not flash.empty?
    assert_template 'password_resets/new'
    # Valid email
    post password_resets_path, password_reset: { email: @user.email }
    assert_not_equal @user.reset_digest, @user.reload.reset_digest
    assert_equal 1, ActionMailer::Base.deliveries.size
    assert_not flash.empty?
    assert_redirected_to login_url
    # Password reset form
    user = assigns(:user)
    # Wrong email
    get edit_password_reset_path(user.reset_token, email: "")
    assert_redirected_to login_url
    # Right email, wrong token
    get edit_password_reset_path('wrong token', email: user.email)
    assert_redirected_to login_url
    # Right email, right token
    get edit_password_reset_path(user.reset_token, email: user.email)
    assert_template 'password_resets/edit'
    assert_select "input[name=email][type=hidden][value=?]", user.email
    # Invalid password & confirmation
    patch password_reset_path(user.reset_token),
          email: user.email,
          user: { password:              "foobaz",
                  password_confirmation: "barquux" }
    assert_select 'div#error_explanation'
    # Empty password
    patch password_reset_path(user.reset_token),
          email: user.email,
          user: { password:              "",
                  password_confirmation: "" }
    assert_not flash.empty?
    assert_template 'password_resets/edit'
    # Valid password & confirmation
    patch password_reset_path(user.reset_token),
          email: user.email,
          user: { password:              "foobaz",
                  password_confirmation: "foobaz" }
    assert is_logged_in?
    assert_not flash.empty?
    assert_redirected_to user
  end
end

I've googled a lot and know that this error is caused because there is no @user instance variable in the edit method from the password controller. However, I know that there is one because a before filter is called before edit, which assigns @user to User.find_by(email: params[:email]).

Why is this happening? Let me know if I need to add something else.

Aucun commentaire:

Enregistrer un commentaire