如何用多态设计用户模型解决未定义的方法`model_name'?

时间:2014-02-24 09:04:13

标签: ruby-on-rails devise

我正在使用Rails 4和Ruby 2进行注册 我已经定义了一个User模型,它与Speaker或Organization有多态关系(我还没有编写这个模型)。我已经创建了自己的Devise RegistrationController,用于在初始化User对象时初始化相应的对象(Speaker)。根据特殊路线。我已经使用Speaker对象的字段编辑了新的注册表单。 我想要实现的是当用户填写所有字段(来自User的字段和来自Speaker的字段)时,会创建适当的对象。因此,User对象具有指向Speaker对象的链接。但是,当我提交表单时出现以下错误,但我得到了以下错误,我当前的实现:

undefined method `model_name' for NilClass:Class

提取的来源(第35行):

<%=
fields_for resource.identifiable do | identifiable_fields |
render "#{resource.identifiable.class.name.downcase}_fields", f: identifiable_fields
end
%> 

模型如下:

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  belongs_to :identifiable, :polymorphic => true
end
class Speaker < ActiveRecord::Base

    has_one :user, as: :identifiable

    validates :first_name, :presence => true, length: { in: 2..20 }
    validates :last_name,  :presence => true, length: { in: 2..20 }

    def full_name
        [first_name, last_name].join(' ')
    end

    def full_name_reverse
        [last_name, first_name].join(', ')
    end
end

UserRegistrationsController:

class UserRegistrationsController < Devise::RegistrationsController

    # Devise RegistrationsController override.
    # version 3.2.3

    def new
         head :not_implemented and return unless is_identifiable_name?(params[:identifiable_name])
         build_resource_with_identity({},params[:identifiable_name])
         respond_with self.resource
    end

    def create
        build_resource(sign_up_params)
        puts sign_up_params
        if resource.save
            yield resource if block_given?
            if resource.active_for_authentication?
                set_flash_message :notice, :signed_up if is_flashing_format?
                sign_up(resource_name, resource)
                respond_with resource, :location => after_sign_up_path_for(resource)
            else
                set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
                expire_data_after_sign_in!
                respond_with resource, :location => after_inactive_sign_up_path_for(resource)
            end
        else
            clean_up_passwords resource
            respond_with resource
        end
    end


    private

    def build_resource_with_identity(hash = nil, identifiable_name)
        self.resource = resource_class.new_with_session(hash || {}, session)
        self.resource.identifiable = identifiable_name.downcase.camelize.constantize.new
    end

    def is_identifiable_name?(identifiable_name)
       ['speaker'].include? identifiable_name.downcase
    end

end

UserRegistrationsController的路由#new

get 'speakers/register', to: 'user_registrations#new', defaults: { identifiable_name: 'speaker' }, as: :new_speaker_registration

新用户注册表单:

<h2>Registreren</h2>

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>

    <%= devise_error_messages! %>

    <div class="row">
        <div class="small-3 columns">
            <%= f.label :email, 'Email', class: "right inline" %>
        </div>
        <div class="small-9 columns">
            <%= f.email_field :email, :autofocus => true %>
        </div>
    </div>

   <div class="row">
       <div class="small-3 columns">
           <%= f.label :password, 'Wachtwoord', class: "right inline" %>
       </div>
       <div class="small-9 columns">
           <%= f.password_field :password, :autofocus => true %>
       </div>
   </div>

   <div class="row">
       <div class="small-3 columns">
           <%= f.label :password_confirmation, 'Bevestig wachtwoord', class: "right inline" %>
       </div>
       <div class="small-9 columns">
           <%= f.password_field :password_confirmation, :autofocus => true %>
       </div>
   </div>

    <%=
        fields_for resource.identifiable do | identifiable_fields |
            render "#{resource.identifiable.class.name.downcase}_fields", f: identifiable_fields
        end
    %>

    <div class="row">
        <div class="small-9 small-offset-3">
            <%= f.submit "Registreren", class: "small button radius" %>
        </div>
    </div>

<% end %>

<%= render "devise/shared/links" %>

扬声器字段部分:

      <div class="row">
    <div class="small-3 columns">
        <%= f.label :first_name, 'Voornaam', class: "right inline" %>
    </div>
    <div class="small-9 columns">
        <%= f.text_field :first_name, :autofocus => true %>
    </div>
</div>

<div class="row">
    <div class="small-3 columns">
        <%= f.label :last_name, 'Achternaam', class: "right inline" %>
    </div>
    <div class="small-9 columns">
        <%= f.text_field :last_name, :autofocus => true %>
    </div>
</div>

日志:

应用程序跟踪:

app/views/user_registrations/new.html.erb:35:in `block in _app_views_user_registrations_new_html_erb___3888261175776937705_70060949603300'
app/views/user_registrations/new.html.erb:3:in `_app_views_user_registrations_new_html_erb___3888261175776937705_70060949603300'
app/controllers/user_registrations_controller.rb:28:in `create'

发送的参数:

{"utf8"=>"✓",
 "authenticity_token"=>"vDLeKE+CJvzZa/GDcE9KqvV8jszWhc5uPBvBgBkTjO0=",
 "user"=>{"email"=>"",
 "password"=>"[FILTERED]",
 "password_confirmation"=>"[FILTERED]"},
 "speaker"=>{"first_name"=>"",
 "last_name"=>""},
 "commit"=>"Registreren"}

1 个答案:

答案 0 :(得分:0)

我已经能够解决未定义方法的问题:

undefined method `model_name' for NilClass:Class

正如成员sevenseacat所述,在提交表单时,resource.identifiable UserRegistrationsController create 方法中为nil。解决方案是确保在resource.identifiable方法中使用适当的对象设置create,因为我在 User 模型中使用了多态关联。例如,如果我想注册扬声器User.identifiable(用户是设计模型)必须返回扬声器对象。


请注意,在编写解决方案时,我已经重写/优化了一些代码,而不是我在答案中发布的代码。


首先,我更改了 routes.rb 。 我已经在默认哈希中给出了它应该与User模型关联的模型的名称。

get 'speakers/register', to: 'user_registrations#new', defaults: { identifiable_type: 'Speaker' }, as: :new_speaker_registration

get 'organizations/register', to: 'user_registrations#new', defaults: { identifiable_type: 'Organization' }, as: :new_organization_registration

user_registrations_controller.rb 中,我改变了一些事情。

新方法new方法中,我正在检查默认哈希中的参数是否具有值 Speaker Organization ,以确保它们是允许的唯一值。 在构建通常的 User 对象之后,我通过调用constantize构建适当的可识别( Speaker Organization )对象,然后new

创建方法 在create方法中,devise将使用其参数构建User对象。 然后,我将获取需要与 User 对象关联的对象的名称。对象的名称通过表单传递。然后对此名称进行了公式化,以便可以在其上调用new

Rails 4中的

默认情况下不允许进行质量分配。您必须指定允许的属性。使用方法identifiable_resource_params,我将根据对象的名称检查要传入的参数。


class UserRegistrationsController < Devise::RegistrationsController

    # Devise RegistrationsController override.
    # version 3.2.3

    def new 
         head :not_implemented and return unless is_identifiable_type?(params[:identifiable_type])
         build_resource
         self.resource.identifiable = params[:identifiable_type].constantize.new       
         respond_with self.resource
        end

    def create
        build_resource(sign_up_params)
        identifiable_type = params[:user][:identifiable_type].to_s
        resource.identifiable = identifiable_type.constantize.new(identifiable_resource_params(identifiable_type))
        if resource.save
            yield resource if block_given?
            if resource.active_for_authentication?
                set_flash_message :notice, :signed_up if is_flashing_format?
                sign_up(resource_name, resource)
                respond_with resource, :location => after_sign_up_path_for(resource)
            else
                set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
                expire_data_after_sign_in!
                respond_with resource, :location => after_inactive_sign_up_path_for(resource)
            end
        else
            clean_up_passwords resource
            respond_with resource
        end
    end

    private

    def is_identifiable_type? identifiable_type
        ['Speaker','Organization'].include? identifiable_type
    end

    def is_speaker_type? identifiable_type
        'Speaker'.eql?(identifiable_type)
    end

    def is_organization_type? identifiable_type
        'Organization'.eql?(identifiable_type)
    end

    def identifiable_resource_params identifiable_type
        if is_speaker_type? identifiable_type
            params.require(identifiable_type.underscore.to_sym).permit(:first_name, :last_name)
        elsif is_organization_type? identifiable_type
            params.require(identifiable_type.underscore.to_sym).permit(:name, :description, :category, :contact_first_name, :contact_last_name, :contact_email)
        end
    end

end

new.html.erb 中,我添加了两件事。 第一个是一些代码,用于呈现包含 Speaker Organization 对象的特定字段的相应部分。其次,我确保需要与 User 对象关联的对象的名称被传递,因此控制器中的create方法知道它。