Search code examples
ruby-on-railsrailstutorial.orgnomethoderror

NoMethodError in UsersController#show after updating users attributes


I am using the railstutorial.org book. I tried updating the user attributes as written in Chapter 7 of the book, but the email became nil. I have tried updating to no avail. This produces a NoMethodError in UsersController#show: undefined method `downcase' for nil:NilClass.

Here is the show.html.erb

<% provide(:title, @user.name) %>
<h1>
  <%= gravatar_for @user %>
  <%= @user.name %>
</h1>

Users helper

module UsersHelper
  # Returns the Gravatar of the given user.
  def gravatar_for(user)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

User Controller

class UsersController < ApplicationController
  def new
  end

  def show
    @user = User.find(params[:id])
  end
end

User model

class User < ActiveRecord::Base
  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
  validates :email, presence: true,
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }
end

Please I need help to fix this. Thanks.


Solution

  • I'm also following this tutorial, so I don't pretend to be a Rails expert.

    That said, I just went back and reviewed the tutorial material surrounding the first introduction of the

    before_save { email.downcase! }
    

    syntax, and I see this at the end of Chapter 6 (listing 6.42).

    I'm pretty sure this isn't working for you because your UsersController is missing a definition of the "New" method:

        def new
           @user = User.new
        end
    

    I'm wiling to bet that your @user object is Nil because you haven't create an instance of it yet. BTW, at this point in the tutorial, you should have also defined a Create method in UsersController.

    EDIT: If your problems are limited to what is happening in the Rails console, I agree with the comments that you need to provide a complete transcript of your console session in order for folks to provide a complete answer.

    Here's an example Rails console session from within my Rails Tutorial project:

    1. Invoke the console and make it aware of my User model:

      $rails console  
      Loading development environment (Rails 4.2.2)  
      require './app/models/user'  
      => true  
      
    2. Create a User instance named "spong"

      **spong = User.new**  
      => <User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil, admin: nil, activation_digest: nil, activated: false, activated_at: nil>  
      

    (Note: My User model has more attributes because I am toward the end of the Tutorial.)

    1. Populate values for name and e-mail:

      spong.name = "Yo Dawg!"  
      => "Yo Dawg!"  
      spong.email = "YoDaWG@dawg.COM"  
      => "YoDaWG@dawg.COM"
      

    Note that my initial e-mail address is mixed case.

    1. Invoke the downcase method:

      spong.email.downcase 
      => "yodawg@dawg.com"  
      

    This is working for me in the console. Now let's try the update_attributes method:

        spong.update_attributes(name: "The Dude", email: "dude@AbideS.org")
    

    This is straight out of the tutorial, but it doesn't work for me, because at this point in my journey, I have implemented features that prevent this kind of update:

        (6.5ms)  begin transaction  
        User Exists (0.5ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER('dude@AbideS.org') LIMIT 1  
        User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER('dude@AbideS.org') LIMIT 1  
        (0.1ms)  rollback transaction  
        => false  
    

    As Hartl says:

    Note that if any of the validations fail, such as when a password is required to save a record (as implemented in Section 6.3), the call to update_attributes will fail.

    1. So let me try the singular version of this command:

      spong.update_attribute( :email, "dude@AbideS.org")  
      (3.7ms)  begin transaction  
      SQL (4.0ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "activation_digest") VALUES (?, ?, ?, ?, ?)  [["name", "The Dude"], ["email", "dude@abides.org"], ... ]  
      (1.2ms)  commit transaction  
      ==> true  
      spong.email 
      => "dude@abides.org"
      

    Not that the e-mail address is already converted to lower case in the INSERT command--exactly as expected, thanks to that

    before_save { email.downcase! }
    

    we have in our User model.

    But what's with all the DB activity? This is because update_attributes updates a single attribute and saves the record without going through the normal validation procedure (which is why I am able to do this). While research this, I found this excellent discussion about update_attribute and update_attributes. Great stuff!

    OK, so what happens if we try to call update_attribute when the (existing) e-mail address is blank? Let's see:

        newUser = User.new  
        => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil, admin: nil, activation_digest: nil, activated: false, activated_at: nil>
    

    Everything in newUser is nil. Let's try to update the e-mail address:

        newUser.update_attribute(:email, "cOnFuSed@MixecCase.com")**  
        (1.2ms)  begin transaction  
        SQL (3.9ms)  INSERT INTO "users" ("email", "created_at", "updated_at", "activation_digest") VALUES (?, ?, ?, ?)  [["email", "confused@mixeccase.com"], ...]  
        (0.9ms)  commit transaction  
        => true
    

    Again, because of the behavior of update_attribute/update_attributes, my database is updated; somewhat counterintuitively, a record is inserted during this "update" process, but this is because I had not yet saved this (or the first) record to the DB.

    I hope all this helps. At a minimum, I have demonstrated that this DOES work via the console--even with previously 'nil' values (and I learned a ton while doing the research to attempt an answer).