Error when Model.new with attributes #152

Closed
yayugu opened this Issue Oct 12, 2011 · 9 comments

Projects

None yet

6 participants

@yayugu

Error when Model.new with attributes
because of commit: afbd387

def attributes=(attributes)
  model = self.model
  attributes.each do |name, value|
    case name
      when String, Symbol
        if model.allowed_writer_methods.include(setter = "#{name}=")  # <- HERE!
          __send__(setter, value)
        else
          raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
        end
      when Associations::Relationship, Property
        self.persistence_state = persistence_state.set(name, value)
    end
  end
end

model.allowed_writer_methods is nil

stack trace:
NoMethodError:
undefined method include?' for nil:NilClass
# /Users/yayugu/.rvm/gems/ruby-1.9.2-p290/bundler/gems/dm-core-6031e6ef3ee8/lib/dm-core/resource.rb:332:in
block in attributes='
# /Users/yayugu/.rvm/gems/ruby-1.9.2-p290/bundler/gems/dm-core-6031e6ef3ee8/lib/dm-core/resource.rb:329:in each'
# /Users/yayugu/.rvm/gems/ruby-1.9.2-p290/bundler/gems/dm-core-6031e6ef3ee8/lib/dm-core/resource.rb:329:in
attributes='
# /Users/yayugu/.rvm/gems/ruby-1.9.2-p290/bundler/gems/dm-core-6031e6ef3ee8/lib/dm-core/resource.rb:748:in initialize'
# ./spec/user_spec.rb:7:in
new'
# ./spec/user_spec.rb:7:in `block (3 levels) in '

code:
user.rb
# encoding: utf-8
class User
include DataMapper::Resource

    property :id, Serial
    property :name, String, :length => (3..40), :unique => true
    property :email, String, :length => (5..40), :unique => true, :format => :email_address
    property :hashed_password, String
    property :salt, String
    property :created_at, DateTime
    property :updated_at, DateTime
    property :permission_level, Integer, :default => 1

    has n, :entries

    attr_accessor :password, :password_confirmation

    validates_uniqueness_of :name
    validates_uniqueness_of :email
    validates_length_of :password, :minimum => 4, :if => :password_require?
    validates_presence_of :password_confirmation, :if => :password_require?
    validates_confirmation_of :password

    before :valid? do
      self.name = name.strip
    end

    def password=(pass)
      @password = pass
      self.salt = User.random_string(10) if !self.salt
      self.hashed_password = User.encrypt(@password, self.salt) if !@password.blank?
    end

    def self.authenticate(name, pass)
      current_user = first(:name => name)
      return nil if current_user.nil?
      return current_user if User.encrypt(pass, current_user.salt) == current_user.hashed_password
      nil
    end

    def admin?
      permission_level == 1
    end

    def password_require?
      self.new? || (!self.new? && !self.password.blank?)
    end

    protected

    def self.encrypt(pass, salt)
      Digest::SHA1.hexdigest(pass+salt)
    end

    def self.random_string(len)
      Array.new(len) { ['a'..'z','A'..'Z','0'..'9'].map(&:to_a).flatten[rand(61)] }.join
    end
  end

  class Hash
    def stringify
      inject({}) do |options, (key, value)|
        options[key.to_s] = value.to_s
        options
      end
    end

    def stringify!
      each do |key, value|
        delete(key)
        store(key.to_s, value.to_s)
      end
    end
  end

  class GuestUser
    def admin?
      false
    end

    def permission_level
      0
    end
  end

user_spec.rb

        # encoding: utf-8
        describe "User" do
          context "register" do
            before do
              @user = User.new(
                :name => 'Johnny',
                :email => 'johnny@example.com',
                :password => 'password',
                :password_confirmation => 'password'
              )
            end
            after :each do
              @user.destroy
            end
            it "should be able to register a user" do
              @user.save.should be_true
              @user.name.should eq('Johnny')
            end
            it "should strip spaces" do
              @user.name = ' Johnny '
              @user.save.should be_true
              @user.name.should eq('Johnny')
            end
          end
        end

when rollback to
if model.public_method_defined?(setter = "#{name}=")
it works fine.

@namelessjon
DataMapper member

This is because you are failing to call DataMapper.finalize after requiring all your models. This is an important part of setting up the DataMapper models in memory, including initialization of relationships and some basic sanity checking.

If the problem persists after calling DataMapper.finalize, you should reopen.

@yayugu

it works! thanks!!

@disbelief

Could you please advise on where exactly within the Rails app DataMapper.finalize should be called? Can it be called multiple times (ie. at the bottom of every model file), once from inside an initializer, application.rb, etc.?

@namelessjon
DataMapper member

Hi disbelief

DataMapper.finalize is safe to call multiple times at the bottom of each file*, BUT it should really only be called once, after all models have loaded. So in an initializer (the first one?) is ideal, assuming I correctly understand the rails load sequence. I'm not sure where application.rb gets loaded, so I can't advise on that one.

* if the model has a relationship to another model that hasn't been required yet, it will blow up**
** Probably, anyway. Unless rails does some funky autoloading. And even then, I'm not 100% of the implications of running finalize in the middle of an existing finalize call. I think it should be safe, though.

@mbj
DataMapper member

@disbelief

As far as I know :

  • the dm-rails railtie calls DataMapper.finalize after rails loaded the models.
  • you can call DataMapper.finalize as often as you want without any side effect.
@disbelief

@namelessjon I've got something like what you've described working now. I created a DataMapper initializer that requires all of my Model files first, then calls DataMapper.finalize. I heed your warning about things blowing up if the Models require files that haven't been included yet.

@mbj thanks for the info. I'm actually not using the dm-rails gem at the moment. I'm going to check it out now.

@pdf

This will also happen when cache_classes = false in Rails. Which obviously makes testing and development rather tiresome.

@jaischeema

@pdf Did you find a way around that in development environment?

@pdf

@jaischeema Only by explicitly calling .finalize in some codepaths, not very satisfactory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment