使用 Ruby On Rails 的多个用户模型,并设计为具有单独的注册路线但有一个通用的登录路线 [英] Multiple user models with Ruby On Rails and devise to have separate registration routes but one common login route

查看:14
本文介绍了使用 Ruby On Rails 的多个用户模型,并设计为具有单独的注册路线但有一个通用的登录路线的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

首先,我在 Google 和 Yahoo 上进行了大量搜索,发现了一些与我类似的主题的回复,但它们都没有真正涵盖我需要知道的内容.

我的应用中有几个用户模型,现在是客户、设计师、零售商,而且似乎还有更多.他们都有不同的数据存储在他们的表和站点上他们被允许或不允许的几个区域中.所以我想采用 devise+CanCan 的方式,并通过多态关联来试试运气,所以我得到了以下模型设置:

class User <增强现实归属地:可登录,:多态 =>真的结尾类客户<增强现实has_one :user, :as =>:可登录结尾班级设计师增强现实has_one :user, :as =>:可登录结尾类零售商<增强现实has_one :user, :as =>:可登录结尾

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

devise_for :customers, :class_name =>'用户'devise_for :designers, :class_name =>'用户'devise_for : 零售商, :class_name =>'用户'

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

但是通过这种设置,我得到了像 customer_signed_in?designer_signed_in? 这样的助手,但我真正需要的是像 user_signed_in 这样的通用助手?代码>用于所有用户都可以访问的站点区域,无论用户类型如何.

我还想要一个像 new_user_session_path 这样的路由助手,而不是几个 new_*type*_session_path 等等.事实上,我需要与众不同的是注册过程......

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

解决方案

好的,所以我解决了并得出了以下解决方案.
我需要一些服装设计,但没那么复杂.

用户模型

# user.rb类用户真的结尾

客户模型

# customer.rb类客户:可滚动结尾

设计师模型

#designer.rb班级设计师ActiveRecord::Basehas_one :user, :as =>:可滚动结尾

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

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

注册

<%# 自定义代码开始params[:user][:user_type] ||= '客户'如果[客户",设计师"].include?params[:user][:user_type].downcasechild_class_name = params[:user][:user_type].downcase.camelizeuser_type = params[:user][:user_type].downcase别的child_class_name = "客户"user_type = "客户"结尾resource.rolable = child_class_name.constantize.new 如果 resource.rolable.nil?#自定义代码结束%><%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f|%><%= my_devise_error_messages!#自定义代码%><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><%#自定义代码开始%><%= fields_for resource.rolable do |rf|%><%渲染:部分=>"#{child_class_name.underscore}_fields", :locals =>{:f =>rf } %><%结束%><%= hidden_​​field :user, :user_type, :value =>用户类型%><%#自定义代码结束%><div><%= f.submit "Sign up" %></div><%结束%><%=渲染:部分=>设计/共享/链接"%>

对于每个用户类型,我创建了一个单独的部分,其中包含该特定用户类型的自定义字段,即设计者 --> _designer_fields.html

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

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

devise_for :users, :controllers =>{:注册=>'用户注册' }

然后我生成了一个控制器来处理定制的注册过程,从 Devise::RegistrationsController 中的 create 方法复制原始源代码并修改它以工作我的方式(不要忘记将您的视图文件移动到适当的文件夹,在我的情况下 app/views/user_registrations

class UserRegistrationsController <设计::注册控制器定义创建构建资源# 自定义代码开始# 根据给定的用户类型创建一个新的子实例child_class = params[:user][:user_type].camelize.constantizeresource.rolable = child_class.new(params[child_class.to_s.underscore.to_sym])# 首先检查子实例是否有效# 原因如果是这样并且父实例也是有效的# 一次性全部保存有效 = 资源.有效?有效 = resource.rolable.valid?&&有效的#自定义代码结束如果有效 &&resource.save #自定义代码如果resource.active_for_authentication?set_flash_message :notice, :signed_up 如果 is_navigational_format?登录(资源名称,资源)response_with 资源,:location =>重定向位置(资源名称,资源)别的set_flash_message :notice, :inactive_signed_up, :reason =>inactive_reason(resource) 如果 is_navigational_format?expire_session_data_after_sign_in!response_with 资源,:location =>after_inactive_sign_up_path_for(资源)结尾别的clean_up_passwords(资源)response_with_navigational(resource) { render_with_scope :new }结尾结尾结尾

这一切的基本作用是,控制器根据 user_type 参数确定必须创建哪种用户类型,该参数由通过 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 ApplicationHelperdef my_devise_error_messages!如果resource.errors.empty 返回""?&&resource.rolable.errors.empty?消息 = rollable_messages = ""如果 !resource.errors.empty?消息 = resource.errors.full_messages.map { |msg|content_tag(:li, msg) }.join结尾如果 !resource.rolable.errors.empty?rolable_messages = resource.rolable.errors.full_messages.map { |msg|content_tag(:li, msg) }.join结尾消息 = 消息 + rollable_messages句子 = I18n.t("errors.messages.not_saved",:count =>resource.errors.count + resource.rolable.errors.count,:资源 =>resource.class.model_name.human.downcase)html = <<-HTML<div id="error_explanation"><h2>#{sentence}</h2><ul>#{messages}</ul>

HTMLhtml.html_safe结尾结尾

更新:

为了能够支持像 /designer/sign_up/customer/sign_up 这样的路由,您可以在路由文件中执行以下操作:

# routes.rb匹配 'designer/sign_up' =>'user_registrations#new', :user =>{ :user_type =>'设计师' }匹配 'customer/sign_up' =>'user_registrations#new', :user =>{ :user_type =>'顾客' }

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

所以……就是这样.在这里和那里进行了一些调整,我让它以一种非常通用的方式工作,这很容易扩展到许多其他共享公共 User 表的用户模型.

希望有人觉得它有用.

First, I've searched intensely with Google and Yahoo and I've found several replies on topics like mine, but they all don't really cover what I need to know.

I've got several user models in my app, for now it's Customers, Designers, Retailers and it seems there are yet more to come. They all have different data stored in their tables and several areas on the site they're allowed to or not. So I figured to go the devise+CanCan way and to try my luck with polymorphic associations, so I got the following models setup:

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

For the registration I've got customized views for each different User type and my routes are setup like this:

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

For now the registrations controller is left as standard (which is "devise/registrations"), but I figured, since I got different data to store in different models I'd have to customize this behaviour as well!?

But with this setup I got helpers like customer_signed_in? and designer_signed_in?, but what I'd really need is a general helper like user_signed_in? for the areas on the site that are accessible to all users, no matter which user type.

I'd also like a routes helper like new_user_session_path instead of the several new_*type*_session_path and so on. In fact all I need to be different is the registration process...

So I was wondering IF THIS IS THE WAY TO GO for this problem? Or is there a better/easier/less must-customize solution for this?

解决方案

Okay, so I worked it through and came to the following solution.
I needed to costumize devise a little bit, but it's not that complicated.

The User model

# 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

The Customer model

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

The Designer model

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

So the User model has a simple polymorphic association, defining if it's a Customer or a Designer.
The next thing I had to do was to generate the devise views with rails g devise:views to be part of my application. Since I only needed the registration to be customized I kept the app/views/devise/registrations folder only and removed the rest.

Then I customized the registrations view for new registrations, which can be found in app/views/devise/registrations/new.html.erb after you generated them.

<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" %>

For each User type I created a separate partial with the custom fields for that specific User type, i.e. Designer --> _designer_fields.html

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

Then I setup the routes for devise to use the custom controller on registrations

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

Then I generated a controller to handle the customized registration process, copied the original source code from the create method in the Devise::RegistrationsController and modified it to work my way (don't forget to move your view files to the appropriate folder, in my case 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

What this all basically does is that the controller determines which user type must be created according to the user_type parameter that's delivered to the controller's create method by the hidden field in the view which uses the parameter by a simple GET-param in the URL.

For example:
If you go to /users/sign_up?user[user_type]=designer you can create a Designer.
If you go to /users/sign_up?user[user_type]=customer you can create a Customer.

The my_devise_error_messages! method is a helper method which also handles validation errors in the associative model, based on the original devise_error_messages! method

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

UPDATE:

To be able to support routes like /designer/sign_up and /customer/sign_up you can do the following in your routes file:

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

Any parameter that's not used in the routes syntax internally gets passed to the params hash. So :user gets passed to the params hash.

So... that's it. With a little tweeking here and there I got it working in a quite general way, that's easily extensible with many other User models sharing a common User table.

Hope someone finds it useful.

这篇关于使用 Ruby On Rails 的多个用户模型,并设计为具有单独的注册路线但有一个通用的登录路线的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆