From 2103a673f0e0e8be731613aee629e779434a20ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 30 Jun 2010 12:41:44 +0200 Subject: [PATCH] Allow devise_for to be scoped with the scope method. This commit requires latest commits in Rails master. --- lib/devise.rb | 6 ++ lib/devise/mapping.rb | 11 +-- lib/devise/rails/routes.rb | 119 ++++++++++++++++++++------------ test/mapping_test.rb | 5 -- test/rails_app/config/routes.rb | 17 +++-- 5 files changed, 96 insertions(+), 62 deletions(-) diff --git a/lib/devise.rb b/lib/devise.rb index b19da55d..d7253546 100644 --- a/lib/devise.rb +++ b/lib/devise.rb @@ -149,6 +149,7 @@ module Devise mattr_accessor :token_authentication_key @@token_authentication_key = :auth_token + # Which formats should be treated as navigational. mattr_accessor :navigational_formats @@navigational_formats = [:html] @@ -157,9 +158,14 @@ module Devise @@warden_config = nil @@warden_config_block = nil + # When set to true, signing out an user signs out all other scopes. mattr_accessor :sign_out_all_scopes @@sign_out_all_scopes = false + # When set to true, optional segments in Devise no longer raises an error. + mattr_accessor :ignore_optional_segments + @@ignore_otional_segments = false + # Default way to setup Devise. Run rails generate devise_install to create # a fresh initializer with all configuration values. def self.setup diff --git a/lib/devise/mapping.rb b/lib/devise/mapping.rb index a0a1e077..c76f40de 100644 --- a/lib/devise/mapping.rb +++ b/lib/devise/mapping.rb @@ -70,6 +70,12 @@ module Devise @path_prefix = "/#{options.delete(:path_prefix)}/".squeeze("/") + if @path_prefix =~ /\(.*\)/ && Devise.ignore_optional_segments != true + raise ScriptError, "It seems that you are scoping devise_for with an optional segment #{@path_prefix.inspect} " << + "which Devise does not support. Please remove the optional segment or alternatively, if you are *sure* of " << + "what you are doing, you can set config.ignore_optional_segments = true in your devise initializer." + end + @controllers = Hash.new { |h,k| h[k] = "devise/#{k}" } @controllers.merge!(options.delete(:controllers) || {}) @@ -111,11 +117,6 @@ module Devise self.path_prefix.count("/") end - # Returns the raw path using path_prefix and as. - def full_path - path_prefix + path.to_s - end - def authenticatable? @authenticatable ||= self.modules.any? { |m| m.to_s =~ /authenticatable/ } end diff --git a/lib/devise/rails/routes.rb b/lib/devise/rails/routes.rb index f93854d2..b04e5874 100644 --- a/lib/devise/rails/routes.rb +++ b/lib/devise/rails/routes.rb @@ -15,9 +15,10 @@ module ActionDispatch::Routing # generate all needed routes for devise, based on what modules you have # defined in your model. # - # Examples: Let's say you have an User model configured to use - # authenticatable, confirmable and recoverable modules. After creating this - # inside your routes: + # ==== Examples + # + # Let's say you have an User model configured to use authenticatable, + # confirmable and recoverable modules. After creating this inside your routes: # # devise_for :users # @@ -25,20 +26,22 @@ module ActionDispatch::Routing # needed routes: # # # Session routes for Authenticatable (default) - # new_user_session GET /users/sign_in {:controller=>"sessions", :action=>"new"} - # user_session POST /users/sign_in {:controller=>"sessions", :action=>"create"} - # destroy_user_session GET /users/sign_out {:controller=>"sessions", :action=>"destroy"} + # new_user_session GET /users/sign_in {:controller=>"devise/sessions", :action=>"new"} + # user_session POST /users/sign_in {:controller=>"devise/sessions", :action=>"create"} + # destroy_user_session GET /users/sign_out {:controller=>"devise/sessions", :action=>"destroy"} # # # Password routes for Recoverable, if User model has :recoverable configured - # new_user_password GET /users/password/new(.:format) {:controller=>"passwords", :action=>"new"} - # edit_user_password GET /users/password/edit(.:format) {:controller=>"passwords", :action=>"edit"} - # user_password PUT /users/password(.:format) {:controller=>"passwords", :action=>"update"} - # POST /users/password(.:format) {:controller=>"passwords", :action=>"create"} + # new_user_password GET /users/password/new(.:format) {:controller=>"devise/passwords", :action=>"new"} + # edit_user_password GET /users/password/edit(.:format) {:controller=>"devise/passwords", :action=>"edit"} + # user_password PUT /users/password(.:format) {:controller=>"devise/passwords", :action=>"update"} + # POST /users/password(.:format) {:controller=>"devise/passwords", :action=>"create"} # # # Confirmation routes for Confirmable, if User model has :confirmable configured - # new_user_confirmation GET /users/confirmation/new(.:format) {:controller=>"confirmations", :action=>"new"} - # user_confirmation GET /users/confirmation(.:format) {:controller=>"confirmations", :action=>"show"} - # POST /users/confirmation(.:format) {:controller=>"confirmations", :action=>"create"} + # new_user_confirmation GET /users/confirmation/new(.:format) {:controller=>"devise/confirmations", :action=>"new"} + # user_confirmation GET /users/confirmation(.:format) {:controller=>"devise/confirmations", :action=>"show"} + # POST /users/confirmation(.:format) {:controller=>"devise/confirmations", :action=>"create"} + # + # ==== Options # # You can configure your routes with some options: # @@ -62,19 +65,6 @@ module ActionDispatch::Routing # # devise_for :users, :path_names => { :sign_in => 'login', :sign_out => 'logout', :password => 'secret', :confirmation => 'verification' } # - # * :path_prefix => the path prefix to be used in all routes. - # - # devise_for :users, :path_prefix => "/:locale" - # - # If you are using a dynamic prefix, like :locale above, you need to configure default_url_options in your ApplicationController - # class level, so Devise can pick it: - # - # class ApplicationController < ActionController::Base - # def self.default_url_options - # { :locale => I18n.locale } - # end - # end - # # * :controllers => the controller which should be used. All routes by default points to Devise controllers. # However, if you want them to point to custom controller, you should do: # @@ -84,8 +74,46 @@ module ActionDispatch::Routing # # devise_for :users, :skip => :sessions # + # ==== Scoping + # + # Following Rails 3 routes DSL, you can nest devise_for calls inside a scope: + # + # scope "/my" do + # devise_for :users + # end + # + # However, since Devise uses the request path to retrieve the current user, this has a few caveats. + # First, if you are using a dynamic segment, as below: + # + # scope ":locale" do + # devise_for :users + # end + # + # You are required to configure default_url_options in your ApplicationController class level, so + # Devise can pick it: + # + # class ApplicationController < ActionController::Base + # def self.default_url_options + # { :locale => I18n.locale } + # end + # end + # + # Finally, Devise does not (and cannot) support optional segments, either static or dynamic. That + # said, the following does not work: + # + # scope "(/:locale)" do + # devise_for :users + # end + # def devise_for(*resources) options = resources.extract_options! + + if options.key?(:path_prefix) + ActiveSupport::Deprecation.warn "Giving :path_prefix to devise_for is deprecated and has no effect. " << + "Please use scope from the new router DSL instead." + end + + options[:path_prefix] = @scope[:path] resources.map!(&:to_sym) resources.each do |resource| @@ -106,12 +134,18 @@ module ActionDispatch::Routing routes = mapping.routes routes -= Array(options.delete(:skip)).map { |s| s.to_s.singularize.to_sym } - routes.each do |mod| - send(:"devise_#{mod}", mapping, mapping.controllers) + scope mapping.path.to_s, :as => mapping.name do + routes.each { |mod| send(:"devise_#{mod}", mapping, mapping.controllers) } end end end + # Allow you to add authentication request from the router: + # + # authenticate(:user) do + # resources :post + # end + # def authenticate(scope) constraint = lambda do |request| request.env["warden"].authenticate!(:scope => scope) @@ -125,36 +159,31 @@ module ActionDispatch::Routing protected def devise_session(mapping, controllers) - scope mapping.full_path do - get mapping.path_names[:sign_in], :to => "#{controllers[:sessions]}#new", :as => :"new_#{mapping.name}_session" - post mapping.path_names[:sign_in], :to => "#{controllers[:sessions]}#create", :as => :"#{mapping.name}_session" - get mapping.path_names[:sign_out], :to => "#{controllers[:sessions]}#destroy", :as => :"destroy_#{mapping.name}_session" + scope :controller => controllers[:sessions], :as => :session do + get :new, :path => mapping.path_names[:sign_in] + post :create, :path => mapping.path_names[:sign_in], :as => "" + get :destroy, :path => mapping.path_names[:sign_out] end end def devise_password(mapping, controllers) - scope mapping.full_path, :as => mapping.name do - resource :password, :only => [:new, :create, :edit, :update], :path => mapping.path_names[:password], :controller => controllers[:passwords] - end + resource :password, :only => [:new, :create, :edit, :update], + :path => mapping.path_names[:password], :controller => controllers[:passwords] end def devise_confirmation(mapping, controllers) - scope mapping.full_path, :as => mapping.name do - resource :confirmation, :only => [:new, :create, :show], :path => mapping.path_names[:confirmation], :controller => controllers[:confirmations] - end + resource :confirmation, :only => [:new, :create, :show], + :path => mapping.path_names[:confirmation], :controller => controllers[:confirmations] end def devise_unlock(mapping, controllers) - scope mapping.full_path, :as => mapping.name do - resource :unlock, :only => [:new, :create, :show], :path => mapping.path_names[:unlock], :controller => controllers[:unlocks] - end + resource :unlock, :only => [:new, :create, :show], + :path => mapping.path_names[:unlock], :controller => controllers[:unlocks] end def devise_registration(mapping, controllers) - scope mapping.full_path[1..-1], :as => mapping.name do - resource :registration, :only => [:new, :create, :edit, :update, :destroy], :path => mapping.path_names[:registration], - :path_names => { :new => mapping.path_names[:sign_up] }, :controller => controllers[:registrations] - end + resource :registration, :only => [:new, :create, :edit, :update, :destroy], :path => mapping.path_names[:registration], + :path_names => { :new => mapping.path_names[:sign_up] }, :controller => controllers[:registrations] end def raise_no_devise_method_error!(klass) diff --git a/test/mapping_test.rb b/test/mapping_test.rb index dc1b358a..b19f1de6 100644 --- a/test/mapping_test.rb +++ b/test/mapping_test.rb @@ -97,11 +97,6 @@ class MappingTest < ActiveSupport::TestCase assert_equal 2, Devise.mappings[:manager].segment_position end - test 'path is returned with path prefix and as' do - assert_equal '/users', Devise.mappings[:user].full_path - assert_equal '/:locale/accounts', Devise.mappings[:manager].full_path - end - test 'magic predicates' do mapping = Devise.mappings[:user] assert mapping.authenticatable? diff --git a/test/rails_app/config/routes.rb b/test/rails_app/config/routes.rb index 8d6f09d8..1b7d8916 100644 --- a/test/rails_app/config/routes.rb +++ b/test/rails_app/config/routes.rb @@ -8,13 +8,16 @@ Rails::Application.routes.draw do devise_for :users devise_for :admin, :path => "admin_area", :controllers => { :sessions => "sessions" }, :skip => :passwords - devise_for :accounts, :singular => "manager", :path_prefix => ":locale", :class_name => "User", - :path_names => { - :sign_in => "login", :sign_out => "logout", - :password => "secret", :confirmation => "verification", - :unlock => "unblock", :sign_up => "register", - :registration => "management" - } + + scope ":locale" do + devise_for :accounts, :singular => "manager", :class_name => "User", + :path_names => { + :sign_in => "login", :sign_out => "logout", + :password => "secret", :confirmation => "verification", + :unlock => "unblock", :sign_up => "register", + :registration => "management" + } + end match "/admin_area/home", :to => "admins#index", :as => :admin_root match "/sign_in", :to => "devise/sessions#new"