具有Ruby On Rails的多个用户模型,并设计为具有单独的注册路由,但具有一个通用登录路由

时间:2011-09-04 13:50:46

标签: ruby-on-rails devise multiple-models

首先,我与谷歌和雅虎进行了密切的搜索,我发现了一些关于像我这样的话题的回复,但它们并没有真正涵盖我需要知道的内容。

我的应用程序中有几个用户模型,现在它是客户,设计师,零售商,似乎还有更多的用户模型。它们都有不同的数据存储在他们的表格中,以及他们允许或不允许的网站上的几个区域。所以我想去设计+ CanCan的方式,并尝试我的运气与多态关联,所以我得到了以下模型设置:

class User < AR
  belongs_to :loginable, :polymorphic => true
end

class Customer < AR
  has_one :user, :as => :loginable
end

class Designer < AR
  has_one :user, :as => :loginable
end

class Retailer < AR
  has_one :user, :as => :loginable
end

对于注册,我为每种不同的用户类型提供了自定义视图,我的路由设置如下:

devise_for :customers, :class_name => 'User'
devise_for :designers, :class_name => 'User'
devise_for :retailers, :class_name => 'User'

现在注册控制器是标准的(“设计/注册”),但我想,因为我有不同的数据存储在不同的模型中,我也必须自定义这种行为!?

但是通过这种设置,我得到了customer_signed_in?designer_signed_in?这样的帮助,但我真正需要的是像user_signed_in?这样的通用助手,用于网站上可以访问的区域所有用户,无论哪种用户类型。

我也喜欢像new_user_session_path这样的路线助手而不是几个new_*type*_session_path等等。事实上,我需要做的就是注册过程......

所以我想知道如果这是解决这个问题的方法吗?或者是否有更好/更容易/更少必须定制的解决方案???

提前致谢,
罗伯特

3 个答案:

答案 0 :(得分:35)

好的,所以我完成了工作并找到了以下解决方案 我需要对设计进行一点点设计,但这并不复杂。

用户模型

# user.rb
class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  attr_accessible :email, :password, :password_confirmation, :remember_me

  belongs_to :rolable, :polymorphic => true
end

客户模式

# customer.rb
class Customer < ActiveRecord::Base
  has_one :user, :as => :rolable
end

Designer模型

# designer.rb
class Designer < ActiveRecord::Base
  has_one :user, :as => :rolable
end

因此,用户模型具有简单的多态关联,定义它是客户还是设计师 我接下来要做的就是使用rails g devise:views生成设计视图,使其成为我的应用程序的一部分。由于我只需要自定义注册,因此我只保留app/views/devise/registrations文件夹并删除其余部分。

然后我自定义了新注册的注册视图,可以在生成注册后在app/views/devise/registrations/new.html.erb中找到。

<h2>Sign up</h2>

<%
  # customized code begin

  params[:user][:user_type] ||= 'customer'

  if ["customer", "designer"].include? params[:user][:user_type].downcase
    child_class_name = params[:user][:user_type].downcase.camelize
    user_type = params[:user][:user_type].downcase
  else
    child_class_name = "Customer"
    user_type = "customer"
  end

  resource.rolable = child_class_name.constantize.new if resource.rolable.nil?

  # customized code end
%>

<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %>
  <%= my_devise_error_messages!    # customized code %>

  <div><%= f.label :email %><br />
  <%= f.email_field :email %></div>

  <div><%= f.label :password %><br />
  <%= f.password_field :password %></div>

  <div><%= f.label :password_confirmation %><br />
  <%= f.password_field :password_confirmation %></div>

  <% # customized code begin %>
  <%= fields_for resource.rolable do |rf| %>
    <% render :partial => "#{child_class_name.underscore}_fields", :locals => { :f => rf } %>
  <% end %>

  <%= hidden_field :user, :user_type, :value => user_type %>
  <% # customized code end %>

  <div><%= f.submit "Sign up" %></div>
<% end %>

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

对于每个用户类型,我使用该特定用户类型的自定义字段创建了一个单独的部分,即Designer - &gt; _designer_fields.html

<div><%= f.label :label_name %><br />
<%= f.text_field :label_name %></div>

然后我设置路由以便在注册时使用自定义控制器

devise_for :users, :controllers => { :registrations => 'UserRegistrations' }

然后我生成了一个控制器来处理自定义注册过程,从create中的Devise::RegistrationsController方法复制原始源代码并修改它以我的方式工作(别忘了移动你的在我的案例app/views/user_registrations

中查看文件到相应的文件夹
class UserRegistrationsController < Devise::RegistrationsController
  def create
    build_resource

    # customized code begin

    # crate a new child instance depending on the given user type
    child_class = params[:user][:user_type].camelize.constantize
    resource.rolable = child_class.new(params[child_class.to_s.underscore.to_sym])

    # first check if child instance is valid
    # cause if so and the parent instance is valid as well
    # it's all being saved at once
    valid = resource.valid?
    valid = resource.rolable.valid? && valid

    # customized code end

    if valid && resource.save    # customized code
      if resource.active_for_authentication?
        set_flash_message :notice, :signed_up if is_navigational_format?
        sign_in(resource_name, resource)
        respond_with resource, :location => redirect_location(resource_name, resource)
      else
        set_flash_message :notice, :inactive_signed_up, :reason => inactive_reason(resource) if is_navigational_format?
        expire_session_data_after_sign_in!
        respond_with resource, :location => after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords(resource)
      respond_with_navigational(resource) { render_with_scope :new }
    end
  end
end

这基本上做的是控制器根据使用参数的视图中的隐藏字段传递给控制器​​的user_type方法的create参数来确定必须创建哪个用户类型通过URL中的简单GET-param。

例如:
如果你转到/users/sign_up?user[user_type]=designer,你可以创建一个设计师 如果您转到/users/sign_up?user[user_type]=customer,则可以创建客户。

my_devise_error_messages!方法是一种辅助方法,它还根据原始devise_error_messages!方法处理关联模型中的验证错误

module ApplicationHelper
  def my_devise_error_messages!
    return "" if resource.errors.empty? && resource.rolable.errors.empty?

    messages = rolable_messages = ""

    if !resource.errors.empty?
      messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
    end

    if !resource.rolable.errors.empty?
      rolable_messages = resource.rolable.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
    end

    messages = messages + rolable_messages   
    sentence = I18n.t("errors.messages.not_saved",
                      :count => resource.errors.count + resource.rolable.errors.count,
                      :resource => resource.class.model_name.human.downcase)

    html = <<-HTML
    <div id="error_explanation">
    <h2>#{sentence}</h2>
    <ul>#{messages}</ul>
    </div>
    HTML

    html.html_safe
  end
end

更新:

为了能够支持/designer/sign_up/customer/sign_up等路线,您可以在路线文件中执行以下操作:

# routes.rb
match 'designer/sign_up' => 'user_registrations#new', :user => { :user_type => 'designer' }
match 'customer/sign_up' => 'user_registrations#new', :user => { :user_type => 'customer' }

在内部路由语法中未使用的任何参数都会传递给params哈希。所以:user被传递给params哈希。

所以......就是这样。通过这里和那里的一些小小的推文,我得到了一个非常通用的方式,这很容易扩展,许多其他用户模型共享一个共同的用户表。

希望有人觉得它很有用。

答案 1 :(得分:6)

我没有找到任何方式评论接受的答案,所以我要写在这里。

有些事情与接受的答案完全不同,可能是因为它已经过时了。

无论如何,我必须自己解决一些问题:

  1. 对于UserRegistrationsController,render_with_scope不再存在,只需使用render :new
  2. createRegistrationsController中的create函数中的第一行也没有按照说明工作。只是尝试使用

    # Getting the user type that is send through a hidden field in the registration form.
    user_type = params[:user][:user_type]
    
    # Deleting the user_type from the params hash, won't work without this.
    params[:user].delete(:user_type)
    
    # Building the user, I assume.
    build_resource
    
  3. 而不仅仅是build_resource。一些质量分配错误在未改变时出现。

    1. 如果您想在Devise的current_user方法中获取所有用户信息,请进行以下修改:
    2. class ApplicationController < ActionController::Base protect_from_forgery

        # Overriding the Devise current_user method
        alias_method :devise_current_user, :current_user
        def current_user
          # It will now return either a Company or a Customer, instead of the plain User.
          super.rolable
        end
      end
      

      # Overriding the Devise current_user method alias_method :devise_current_user, :current_user def current_user # It will now return either a Company or a Customer, instead of the plain User. super.rolable end end

答案 2 :(得分:2)

我遵循了上述说明,发现了一些差距,而且在我实施时,说明已经过时了。

所以在整天挣扎之后,让我与大家分享一下对我有用的东西 - 希望它能为你节省几个小时的汗水和泪水

  • 首先,如果您不熟悉RoR多态性,请仔细阅读本指南: http://astockwell.com/blog/2014/03/polymorphic-associations-in-rails-4-devise/ 在关注它之后,您将安装设备和用户用户模型,您将能够开始工作。

  • 之后请按照Vapire的精彩教程生成所有参与者的视图。

  • 我发现最令人沮丧的是,使用最新版本的Devise(3.5.1),RegistrationController拒绝工作。 以下代码将使其再次起作用:

    def create 
    
      meta_type = params[:user][:meta_type]
      meta_type_params = params[:user][meta_type]
    
      params[:user].delete(:meta_type)
      params[:user].delete(meta_type)
    
      build_resource(sign_up_params)
    
      child_class = meta_type.camelize.constantize
      child_class.new(params[child_class.to_s.underscore.to_sym])
      resource.meta = child_class.new(meta_type_params)
    
      # first check if child intance is valid
      # cause if so and the parent instance is valid as well
      # it's all being saved at once
      valid = resource.valid?
      valid = resource.meta.valid? && valid
    
      # customized code end
      if valid && resource.save    # customized code
        yield resource if block_given?
        if resource.persisted?
          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
          set_minimum_password_length
          respond_with resource
        end
      end
    end
    
  • 并添加这些覆盖,以便重定向可以正常工作:

    protected
    
      def after_sign_up_path_for(resource)
        after_sign_in_path_for(resource)
      end
    
      def after_update_path_for(resource)
        case resource
        when :user, User
          resource.meta? ? another_path : root_path
        else
          super
        end
      end
    
  • 为了使设计Flash消息继续有效,您需要更新config/locales/devise.en.yml而不是UserRegistraionsControlloer重写的RegistraionsControlloer,您需要做的就是添加以下新部分:

    user_registrations:
      signed_up: 'Welcome! You have signed up successfully.'
    

希望能为你们节省几个小时。