36 votes

Modèles d'utilisateurs multiples avec Ruby On Rails et conception d'itinéraires d'enregistrement distincts mais d'un itinéraire de connexion commun

Tout d'abord, j'ai fait des recherches intensives sur Google et Yahoo et j'ai trouvé plusieurs réponses sur des sujets comme le mien, mais elles ne couvrent pas vraiment ce que j'ai besoin de savoir.

J'ai plusieurs modèles d'utilisateurs dans mon application, pour l'instant ce sont les clients, les concepteurs, les détaillants et il semble qu'il y en ait encore d'autres à venir. Ils ont tous des données différentes stockées dans leurs tables et plusieurs zones du site auxquelles ils sont autorisés ou non. J'ai donc décidé de suivre la méthode devise+CanCan et de tenter ma chance avec les associations polymorphes, et j'ai donc configuré les modèles suivants :

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

Pour l'enregistrement, j'ai des vues personnalisées pour chaque type d'utilisateur et mes itinéraires sont configurés comme suit :

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

Pour l'instant, le contrôleur d'enregistrement est laissé tel quel (c'est-à-dire "devise/registrations"), mais je me suis dit que puisque j'avais des données différentes à stocker dans des modèles différents, je devais aussi personnaliser ce comportement ?

Mais avec cette configuration, j'ai obtenu des aides comme customer_signed_in? y designer_signed_in? mais ce dont j'aurais vraiment besoin, c'est d'une aide générale comme user_signed_in? pour les zones du site qui sont accessibles à tous les utilisateurs, quel que soit le type d'utilisateur.

J'aimerais également disposer d'un outil d'aide à l'élaboration des itinéraires tel que new_user_session_path au lieu de plusieurs new_*type*_session_path et ainsi de suite. En fait, tout ce dont j'ai besoin pour être différent, c'est de la procédure d'enregistrement...

Je me demandais donc si c'était la bonne solution pour résoudre ce problème. Ou existe-t-il une meilleure solution, plus facile, moins contraignante à personnaliser ?

0 votes

Quelqu'un a-t-il des suggestions ?

36voto

Vapire Points 2977

Ok, donc j'ai travaillé à travers et est venu à la solution suivante.
J'avais besoin de costumize concevoir un peu, mais ce n'est pas compliqué.

Le modèle de l'Utilisateur

# 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

Le modèle Client

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

Le Concepteur de modèle

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

Donc, le modèle de l'Utilisateur est un simple polymorphe de l'association, de définir si c'est un Client ou d'un Designer.
La prochaine chose que je n'avais plus qu'à générer le dispositif de vues avec rails g devise:views de faire partie de mon application. Depuis que j'ai seulement besoin de l'enregistrement à être personnalisés j'ai gardé l' app/views/devise/registrations le dossier seul et retiré le reste.

Puis je l'ai adapté les enregistrements de la vue pour les nouvelles inscriptions, qui peut être trouvé dans app/views/devise/registrations/new.html.erb après vous les a générés.

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

Pour chaque type d'Utilisateur que j'ai créé une partiels séparés avec les champs personnalisés pour le type d'Utilisateur, c'est à dire le Concepteur --> _designer_fields.html

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

Puis-je configurer les routes pour concevoir l'utilisation de la coutume contrôleur sur les inscriptions

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

Ensuite, j'ai généré un contrôleur pour gérer vos processus d'inscription, copié le code source d'origine de l' create méthode Devise::RegistrationsController et l'a modifié à ma façon (n'oubliez pas déplacer les fichiers vers le dossier approprié dans mon cas 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

Ce que tout fait est que le contrôleur détermine quel type d'utilisateur doit être créé en fonction de l' user_type paramètre qui est livré à la manette create méthode par le champ caché de la vue qui utilise le paramètre par un simple GET-paramètre dans l'URL.

Par exemple:
Si vous allez à l' /users/sign_up?user[user_type]=designer vous pouvez créer un Designer.
Si vous allez à l' /users/sign_up?user[user_type]=customer vous pouvez créer un Client.

L' my_devise_error_messages! méthode est une méthode d'aide, qui gère également les erreurs de validation dans le modèle associatif, basé sur l'original de l' devise_error_messages! méthode

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

Mise à JOUR:

Pour être en mesure de soutenir de tels axes /designer/sign_up et /customer/sign_up vous pouvez effectuer les opérations suivantes dans votre fichier de routes:

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

Aucun paramètre n'est pas utilisé dans les routes de la syntaxe interne de passe pour les paramètres de hachage. Donc, :user est passée à la params de hachage.

Alors... c'est elle. Avec un peu de tweeking ici et là j'ai eu de travail, dans un sens général, qui est facilement extensible avec de nombreux autres Utilisateurs des modèles de partage d'une commune de la table User.

J'espère que quelqu'un le trouve utile.

0 votes

Merci Vapire, c'est la même chose que j'ai fini par faire après avoir lu plus attentivement la documentation sur les itinéraires. Merci encore pour la solution générale - elle fonctionne très bien !

0 votes

Merci beaucoup à Vapire pour cette solution géniale et très flexible ! J'ai juste une question concernant les derniers commentaires. J'ai essayé de personnaliser les routes ayant 'designers/sign_up' et 'customers/sign_up' en les passant dans un bloc devise_scope. Cependant, il renvoie une erreur d'objet nul lorsque j'essaie d'accéder à l'un de ces itinéraires. Dois-je changer quelque chose dans les paramètres ? Merci encore !

0 votes

Pourriez-vous également nous indiquer les champs que vous avez ajoutés dans votre table User (avez-vous user_type:string et rolable_id:integer ?) Cela m'aiderait à bien comprendre ;)

6voto

cgf Points 755

Je n'ai pas réussi à trouver un moyen de commenter la réponse acceptée, je vais donc écrire ici.

Il y a deux ou trois choses qui ne fonctionnent pas exactement comme l'indique la réponse acceptée, probablement parce qu'elle n'est pas à jour.

Quoi qu'il en soit, il y a des choses que j'ai dû régler moi-même :

  1. Pour le UserRegistrationsController, render_with_scope n'existe plus, il suffit d'utiliser render :new
  2. La première ligne de la fonction create, toujours dans le UserRegistrationsController, ne fonctionne pas comme indiqué. Essayez simplement d'utiliser

    # 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

au lieu de simplement build_resource . Une erreur d'attribution de masse apparaissait lorsque le produit était inchangé.

  1. Si vous souhaitez disposer de toutes les informations relatives à l'utilisateur dans la méthode current_user de Devise, procédez aux modifications suivantes :

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`

2voto

Guy Dubrovski Points 854

J'ai suivi les instructions ci-dessus et j'ai découvert quelques lacunes et ces instructions n'étaient tout simplement pas à jour au moment où je les ai mises en œuvre.

Après m'être débattue toute la journée, je vais partager avec vous ce qui a fonctionné pour moi - et j'espère que cela vous épargnera quelques heures de sueur et de larmes.

  • Tout d'abord, si vous n'êtes pas très familier avec le polymorphisme RoR, nous vous invitons à consulter ce guide : http://astockwell.com/blog/2014/03/polymorphic-associations-in-rails-4-devise/ Après l'avoir suivi, vous aurez installé les modèles devise et user users et vous pourrez commencer à travailler.

  • Ensuite, suivez le tutoriel de Vapire pour générer les vues avec toutes les parties.

  • Ce que j'ai trouvé le plus frustrant, c'est qu'en utilisant la dernière version de Devise (3.5.1), RegistrationController a refusé de fonctionner. Voici le code qui le fera fonctionner à nouveau :

    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
  • et ajoutez également ces surcharges pour que les redirections fonctionnent correctement :

    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
  • Pour que les messages flash des appareils continuent à fonctionner, vous devez mettre à jour les éléments suivants config/locales/devise.en.yml au lieu du RegistraionsControlloer surchargé par UserRegistraionsControlloer, il vous suffit d'ajouter cette nouvelle section :

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

J'espère que cela vous fera gagner quelques heures.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X