4

Thanks for answering my previous question, but I ran into a new problem.

I'm creating a custom validator that validates whether a user typed in a clean word. This is used on my UsersController as a validation method.

I am using the Obscenity gem but created some of my own methods to ensure quality data.

Error Message

NoMethodError: Undefined method include? for Nil:NilClass

The problem with this is that my methods work if a record already exists, but they don't work during record creation. I've tried to combat this problem by using

:on => [:create, :update]

but I still receive the same error.

Validation Methods

class MyValidator < ActiveModel::Validator

  def mystery_setup
    @mystery_words = # This is a mystery, I can't tell you.
    @mystery_c = @mystery_words.map(&:capitalize)
    @mystery_u = @mystery_words.map(&:upcase)
    @mysteries = @mystery_words + @mystery_c + @mystery_u
    @new_mysteries = @mysteries.map{|mystery|mystery.tr("A-Za-z", "N-ZA-Mn-za-m")}
 end

  def validate (user)
  mystery_setup
    if Obscenity.profane?(user.name) \ 
    || @new_mysteries.any?{|mystery|user.name.include?(mystery)} \
    || @new_mysteries.any?{|mystery|user.email.include?(mystery)} \
    || @new_mysteries.any?{|mystery|user.password.include?(mystery)}
      user.errors[:name] << 'Error: Please select a different username'
    end
  end
end

User.rb(Model)

class User < ActiveRecord::Base

  include ActiveModel::Validations
  validates_with MyValidator

  has_many :favorites, foreign_key: "user_id", dependent: :destroy
  has_many :pictures, through: :favorites

  has_secure_password
  before_create :create_remember_token

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

  validates_presence_of :name, :password, :email
  validates_uniqueness_of :name, :email
  validates :name, length: { in: 3..20 }
  validates :password, length: { minimum: 6 }
  validates :email, format: { with: VALID_EMAIL_REGEX }, length: { in: 8..50 }

  validates_confirmation_of :password, if: lambda { |m| m.password.present? }

  def User.new_remember_token
    SecureRandom.urlsafe_base64
  end

  def User.digest(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

  private

  def create_remember_token
    self.remember_token = User.digest(User.new_remember_token)
  end

end

I have also tried using an unless statement

def validate (user)
  mystery_setup
  unless User.all.include?(user)
    if (Obscenity.profane?(user.name) 
    || @new_mysteries.any {|mystery|user.name.include?(mystery)})  \
    || @new_mysteries.any?{|mystery|user.email.include?(mystery)} \
    ||   @new_mysteries.any?{|mystery|user.password.include?(mystery)}
        user.errors[:name] << 'Error: Please select a different username'
      end
    end
  end
end

I tried testing if there was a user by using the unless statement but that didn't work either.

Following advice from a similar question here, I changed my migrations file to combat this area.

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name, default: 'new'
      t.string :password, default: 'new'
      t.string :email, default: 'new'

      t.timestamps
    end
  end
end

Question Link

undefined method `include?' for nil:NilClass with partial validation of wizard gem

Reference for Code

http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations

Changing the migration file by changing the default values didn't solve this question so I decided to ask a new question here.

This method works for updating records but not for creating new records.

Help is appreciated. Thank you in advanced.

Edit

Just received an excellent suggestion to pass in the attributes in bracket format. My code now looks like this

def validate (user)
    mystery_setup
    unless User.all.include?(user)
      if (Obscenity.profane?(user[:name]) || 
        @new_mysteries.any?{|mystery|user[:name].include?(mystery)})  \
      ||@new_mysteries.any?{|mystery|user[:email].include?(mystery)}
      ||@new_mysteries.any?{|mystery|user[:password].include?(mystery)}
        user.errors[:name] << 'Error: Please select a different username'
      end
    end
end

Right now, it only has an error with the email and password attributes. If I delete the last two ||@new_mysteries.any? lines, my method works for filtering the name.

I would like to keep this professional though, so I'd like to get it to work with the other two methods. Possibly has to do with my use of parentheses or the || symbol?

Solid progress guys, keep it up.

Edit

Also, if I would like to call these validation methods on other classes, would it be better to put this in a helper file?

New Update

Here is my Users Controller code

class UsersController < ApplicationController
  before_action :signed_in_user, only: [:edit, :update, :destroy]
  before_action :correct_user, only: [:edit, :update]

  def index
    @users = User.all
  end

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

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Congratulations #{@user.name}! You have successfully created an account"
      redirect_to games_path
    else
      render 'new'
    end
  end

  def edit
  end

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

  def favorites
    @user = User.find(current_user)
  end

  def destroy
  end

  private

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

   def signed_in_user
     unless signed_in?
       store_location
       redirect_to signin_url notice: "Please sign in."
     end
   end

   def correct_user
     @user = User.find(params[:id])
     redirect_to(root_url) unless current_user?(@user)
   end
end
Community
  • 1
  • 1
Darkmouse
  • 1,941
  • 4
  • 28
  • 52
  • You have three includes which one is the error being thrown on? – jkeuhlen Aug 05 '14 at 16:36
  • `"#{user.name}#{user.password}#{user.email}".include?(mystery)`? Why do you care about the password by the way? – Victor Moroz Aug 05 '14 at 16:43
  • I'd like to keep my site as clean as possible. I don't want anyone to be offended by anything. Even with a password, it's best to stay on the safe side. – Darkmouse Aug 05 '14 at 18:32
  • have you debugged this to make sure that user.name, user.email, user.password all return the values you expect? maybe one of them isn't being passed through because of strong parameters (just sayin). also, have you tried doing `user[:name].include?`, etc? sometimes I've noticed that attributes can only be accessed like that before validation or creation. – ussferox Aug 09 '14 at 21:39
  • Alright, thank you for that comment. It doesn't completely solve my problem, but it's a good start. Right now, using that notation works for the name attribute, but not for the other two attributes, email and password. – Darkmouse Aug 10 '14 at 17:14

7 Answers7

4
|| @new_mysteries.any?{|mystery|user.name.include?(mystery)} \
|| @new_mysteries.any?{|mystery|user.email.include?(mystery)} \
|| @new_mysteries.any?{|mystery|user.password.include?(mystery)}

This error means that user name, email or password is nil. To deal with it you need to change each line to:

user.name && user.name.include?(mystery)

However highly recommend andand gem, which will allow you to write the above as:

user.name.andand.include?(mystery)
BroiSatse
  • 44,031
  • 8
  • 61
  • 86
  • That might make the error go away but I doubt it would solve the problem. A user isn't getting passed in properly. – jkeuhlen Aug 05 '14 at 16:37
  • @jkeuhlen - This is how you pass user to custom validator. Unless I missed sth obvious (note that error is undefined method include?, not undefined method name, so user object is passed all right) – BroiSatse Aug 05 '14 at 16:40
  • Ah good distinction. The user is there but the attributes are nil. I missed that on first read. I think it still might be a higher level design problem and this will treat the symptom rather than the solution. For example, those fields (at least to me) look like they should never be nil so the check should be irrelevant – jkeuhlen Aug 05 '14 at 16:43
  • Before I found an answer on another SO question, this function was working for updating records but not for creating new records. – Darkmouse Aug 05 '14 at 18:33
4

You could write that like this:

def validate (user)
  mystery_setup
  user.errors[:name] << 'Tsk! Tsk! Please select a different username' if
    Obscenity.profane?(user[:name]) ||
      [:name, :email, :password].product(@new_mysteries).any? { |sym, mystery|
        (str = user.public_send sym) && str.include?(mystery) }
end

Thanks to @Arup for the fix.

If you wished to reduce the number of instance variables, you could change the first line to:

new_mysteries = mystery_setup

and change @new_mysteries to new_mysteries.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Looks like this doesn't work. I get an unexpected , expecting keyword end error in the second statement of the OR block. Maybe we need braces before the |sym, mystery| block? This looks like it has potential though. – Darkmouse Aug 11 '14 at 18:01
  • 1
    Correct you are, Dark. `{}` added. Let me know if it works with the fix. I would have tested, but don't have the data. – Cary Swoveland Aug 11 '14 at 18:04
  • I've tested it for braces and it fixes the error message, but it still only works for the name attribute. password and email still aren't up to par. I'll take a look at it and see what I can do – Darkmouse Aug 11 '14 at 18:07
  • I'm not familiar with `ActiveRecord`. I assumed `user` was a hash, but perhaps not, as I see you use `user.name` rather than `user[:name]`. I'm guessing `user[:sym]` needs to be `user.sym`, but I don't know if that would work with `[:name, :email, :password]`. If you can't fix it, perhaps a reader who knows Rails can. – Cary Swoveland Aug 11 '14 at 18:18
  • It looks like that doesn't work either, Cary. I just played with Rails Console, and while the Users table might look like a hash, it actually isn't. I ran User.is_a?(Hash) and got false. It's the same think for User objects as well. – Darkmouse Aug 11 '14 at 18:24
  • @BroiSatse or Boris, can you tell us what the fix is? – Cary Swoveland Aug 11 '14 at 18:45
  • @CarySwoveland You can try something like `str = user.public_send(sym) && str.include?(mystery)` instead of `user[sym] && user[sym].include?(mystery)`. – Arup Rakshit Aug 11 '14 at 19:25
  • Dark, I've made the fix suggested by @Arup. He's never wrong, but you should test anyway. – Cary Swoveland Aug 11 '14 at 19:27
  • Thank you, Dark. Glad to be of help. @Arup, thanks again for your important contribution. – Cary Swoveland Aug 15 '14 at 18:24
  • @DarkMouse Yes.. We are glad, that you got the solution.. :-) – Arup Rakshit Aug 15 '14 at 18:24
2

try this out:

def validate (user)
  mystery_setup
  unless User.all.include?(user)
    if user.name && user.email && user.password
      if (Obscenity.profane?(user.name)  
      || @new_mysteries.any {|mystery|user.name.include?(mystery)})  \
      || @new_mysteries.any?{|mystery|user.email.include?(mystery)} \
      || @new_mysteries.any?{|mystery|user.password.include?(mystery)}
          user.errors[:name] << 'Error: Please select a different username'
        end
      end
    end
  end
end
Sachin Singh
  • 7,107
  • 6
  • 40
  • 80
  • Sorry, but that doesn't work either. I still receive the same problem. It works for the name attribute, but not for the email and password attributes. – Darkmouse Aug 12 '14 at 15:27
2
class MyValidator < ActiveModel::Validator

  def mystery_setup
    mystery_words = # This is a mystery, I can't tell you.
    mystery_c = mystery_words.map(&:capitalize)
    mystery_u = mystery_words.map(&:upcase)
    mysteries = mystery_words + mystery_c + mystery_u
    mysteries.map{ |mystery| mystery.tr("A-Za-z", "N-ZA-Mn-za-m")}
 end

  def validate (user)
    # No need to pollute the class with instance variables, just pass it back in a return
    new_mysteries = mystery_setup

    if Obscenity.profane?(user.name.to_s) || 
       @new_mysteries.any?{ |mystery| user.name.to_s.include?(mystery) ||
                                      user.email.to_s.include?(mystery) ||
                                      user.password.to_s.include?(mystery)}
      user.errors[:name] << 'Error: Please select a different username'
    end
  end
end
A Fader Darkly
  • 3,516
  • 1
  • 22
  • 28
  • Hey, looks like it still receives the same problem I had from the beginning. The name attribute works, but not the password and email attributes. – Darkmouse Aug 12 '14 at 15:22
  • By not working, you mean...? :) (By the way, I shudder at the UI horror of being presented with "Error: Please select a different username" when my email or password contains something considered to be profane. On that point, I don't see why you're filtering the password. If anyone ever sees a password after it's digested and stored as a hash in the database, you're insecure and need to rethink your approach.) – A Fader Darkly Aug 12 '14 at 15:45
  • To be more precise, exactly what error are you getting, and on what line? `nil.to_s.include?("stuff")` throws no error, so unless `mystery` is not a string, you should be error free in the conditional. The error could possibly be elsewhere in the code...? – A Fader Darkly Aug 12 '14 at 15:58
  • Ah, so it's not the original error, it's that the validator isn't working. I'd need to see more of your code to debug it. – A Fader Darkly Aug 12 '14 at 15:59
  • It looks like a problem with the attributes, but I can't figure out why it doesn't work. Are there any things I could try? – Darkmouse Aug 12 '14 at 16:00
  • If possible, yes. Or if you're using some kind of form object, that too. :) – A Fader Darkly Aug 12 '14 at 16:11
  • Alright, I added the Users Controller code, is there anything else I should add? – Darkmouse Aug 12 '14 at 16:12
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/59209/discussion-between-darkmouse-and-a-fader-darkly). – Darkmouse Aug 12 '14 at 16:12
2

I have refactored the code a bit, let me know if this works:

def validate (user)
  mystery_setup

  if Obscenity.profane?(user.name)
    user.errors[:name] << 'Error: Please select a different username'
  end

  %w(name email password).each do |attr|
    value = user.send(attr)
    if value.present? and @new_mysteries.grep(/#{value}/).present?
      user.errors[attr] << "Error: Please select a different user#{attr}"
    end
  end
end
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
2

You have an error in this part, the first line.

@mystery_words = # This is a mystery, I can't tell you.
@mystery_c = mystery_words.map(&:capitalize)

This should be

@mystery_words = [] # This is a mystery, I can't tell you.
@mystery_c = mystery_words.map(&:capitalize)
engineerDave
  • 3,887
  • 26
  • 28
0

Since nil is the representation of atomic nothingness in Ruby, it never includes anything. The include? method can be simply defined on it as:

def nil.include? arg
  false
end
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74