Best practise creating a shared method between devise SessionsController and RegistrationsController - ruby-on-rails

There are 3 ways to get in my applications with an invite token:
You're already logged in
You have an account, but not logged in
You need to register
Now I'm interested how to handle the last 2 situations in combination with Devise without having to repeat the same functions.
Controller overrides are handled from the routes.rb:
devise_for :users, controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
}
Overriding the after_sign_in/up_path for Sessions and Regitrations:
class Users::SessionsController < Devise::SessionsController
protected
def after_sign_in_path(resource)
handle_invite
super(resource)
end
end
class Users::RegistrationsController < Devise::RegistrationsController
protected
def after_sign_up_path_for(resource)
handle_invite
super(resource)
end
end
Where should I place the handle_invite method?
I'm looking for a solution that I can put the method in my UsersController, because that seems to be the right place to put it.:
class UsersController < ApplicationController
private
def handle_invite
# Some code getting the token and adding the user to a group
end
end
I thought this should work, because it seems that Devise inherits this controller:
class Users::SessionsController < Devise::SessionsController; end
class Devise::SessionsController < DeviseController; end
class DeviseController < Devise.parent_controller.constantize; end
So I expected Devise.parent_controller.constantize to represent UsersController, but for some reason handle_invite can't be called from the child controllers.

If you want to use classical inheritance you would have to actually place that class in the inheritance chain by configuring Devise.parent_controller while not breaking the rest of the chain.
Ruby does not have multiple inheritance. A class may only inherit from a single parent class.
# config/initializers/devise.rb
config.parent_controller = 'UsersController'
class UsersController < DeviseController
private
def handle_invite
# Some code getting the token and adding the user to a group
end
end
But that's not really the best way to solve it since Ruby has horizontal inheritance through modules:
# app/controllers/concerns/invitable.rb
module Invitable
private
def handle_invite
# Some code getting the token and adding the user to a group
end
def after_sign_in_path(resource)
handle_invite
super(resource)
end
end
# Do not use the scope resolution operator (::) for namespace definition!
# See https://github.com/rubocop-hq/ruby-style-guide#namespace-definition
module Users
class SessionsController < ::Devise::SessionsController
include Invitable
end
end
module Users
class RegistrationsController < ::Devise::SessionsController
include Invitable
end
end
This is known as a module mixin. In other languages like Java or PHP it would be called a trait. The module encapsulates a set of methods that can be included in any class and you can also mix modules into other modules.
In Rails lingo module mixins are called concerns - this really just wraps a convention that the app/controllers/concerns and app/models/concerns directories are added to the autoload roots. Which means that rails will look for constants in the top level namespace there.
This is also loosely connected to ActiveSupport::Concern which is syntactic sugar for common ruby idioms. But there is no need to use it unless you're actually using its features.

Related

Access application controller variable before class name

I want to access application_controller variable before class name of another controller
class TodosController < eval("#{subdomain_name.humanize}Controller")
I want to customize my ToDoController to inherit different controllers for different subdomains.
Just make the implementations of the inherited methods depend on the subdomain.
One way of avoiding such methods becoming and if-else-case-jungle is to use separate objects to model the subdomain-specific "policies". Here is one possible solution for implementing a subdomain-specific before filter
class TodosController < ApplicationController
before_filter :require_foobar_is_allowed
# ...
end
Define the filter in application controller (or a concern included to TodosController)
class ApplicationController
# ...
protected
def require_foobar_is_allowed
unless subdomain_policy.foobar_allowed_for?(current_user)
render nothing: true, status: 403
end
end
def subdomain_policy
"#{subdomain_name}Policy".constantize.new
end
end
And then define the implementation in domain-specific policy objects (plain-old Ruby objects), can be placed to e.g., app/models/
class FoobarSubdomainPolicy
def foobar_allowed_for?(user)
# in the foobar subdomain, foobar is allowed for everyone
true
end
end
class BazSubdomainPolicy
def foobar_allowed_for?(user)
# in the baz subdomain, foobar is only allowed for admins
user.admin?
end
end
Depending on what the "inherited" functionality actually looks like, there might also exist ready-made solutions in gems such as Devise.

Rails module scope

Given the following controller structure:
# application_controller.rb
class ApplicationController < ActiveController::Base; end
# pages_controller.rb
class PagesController < ApplicationController; end
# admin/application_controller.rb
module Admin
class ApplicationController < ::ApplicationController; end
end
# admin/pages_controller.rb
module Admin
class PagesController < ApplicationController; end
end
One would expect Admin::PagesController to inherit from Admin::ApplicationController and it does. But I have noticed that sometimes it inherits from ::ApplicationController.
So I decided not to risk it and changed declaration of all controllers in /admin to specifically target Admin::ApplicationController
# admin/pages_controller.rb
module Admin
class PagesController < Admin::ApplicationController; end
end
Okay that works, but from what I know it was correct in the first place. Why Rails inherits from a wrong controller sometimes?
Admin::PagesController sometimes inherits from ApplicationController instead of Admin::ApplicationController despite both being in the same module Admin
The problem here is rails' development mode code loading: in general code is loaded when you try to do something with a constant (eg subclass from it) and that constant doesn't exist. This results in const_missing being called and rails uses it this to try to load the class (for a detailed description see the guide).
If neither ApplicationController nor Admin::ApplicationController exist then when you access your admin pages controller ruby will hit that const_missing and try to load admin/application_controller.rb
However if ApplicationController is already loaded then ruby won't fire const_missing since it perfectly legal for a class in the admin module to inherit from something at the toplevel.
The solution as you say is to make explicit what you are inheriting from. Personally in my own apps I use Admin::BaseController as the base class.
Another option is to use require_dependency to point Rails to the correct file:
# admin/application_controller.rb
require_dependency 'admin/application_controller'
module Admin
class PagesController < ApplicationController
end
end

Extend base DeviseController class with method for callbacks in SessionsController and RegistrationsController

My question is similar to this one, which doesn't have an answer.
I want to add after_filter :identify_with_segment, only: [:create] to both my RegistrationsController < Devise::RegistrationsController and SessionsController < Devise::SessionsController. Since both of these controllers themselves inherit from DeviseController, I thought that a DRY way to do this would be to extend DeviseController and define the method there. However, I keep getting an unitialized constant error.
My code:
class DeviseController < DeviseController
def identify_with_segment
..
end
end
I realize the way this class is defined just looks wrong. I've also tried class DeviseController < Devise::DeviseController but that doesn't work either.
Can anyone explain the proper way to extend the DeviseController that these other controllers depend on?
If you want to open DeviseController, you can just try this:
class DeviseController
def identify_with_segment
# ...
end
end
Lots of confusing things are done in the name of strict DRY-ness, this may be one of those things. Sometimes repeating yourself makes things clear and understandable for the human that comes after you.
Another option would be to put this functionality into a concern, and include that module in your RegistrationsController and SessionsController. Then it's explicit what you're doing and you aren't modifying classes that you don't own. Something like:
# app/controllers/concerns/whatever_it_is_this_is_doing.rb
module WhateverItIsThisIsDoing
extend ActiveSupport::Concern
def identify_with_segment
# ...
end
included do
after_filter :identify_with_segment, only: [:create]
end
end
# app/controllers/registrations_controller.rb
class RegistrationsController
include WhateverItIsThisIsDoing
end
# app/controllers/sessions_controller.rb
class SessionsController
include WhateverItIsThisIsDoing
end

how do you put before filters in modular controllers?

I have several controllers that are in a module:
class SoapTest::DashboardController < ApplicationController
class SoapTest::TestCasesController < ApplicationController
etc.
I want to be able to check if a user has certain permissions for a module, and since I don't have a "parent" controller where the above ones inherit, i thought to put the check in a before filter in applications. But I can't seem to get the module name:
in application controller, i have:
before_filter :check_company_features
def check_company_features
puts controller_name
end
but controller_name just returns "dashboard". I need to get the "SoapTest" clause
Be attention, what you currently call modules actually are namespaces.
The reason why controller_name returns only the class name (and not the fully qualified name) is because Rails explicitly strips the namespaces. You can get them by calling the Ruby #name method on the controller class.
class SoapTest::DashboardController < ApplicationController
before_filter :check_company_features
def check_company_features
puts controller_name
# => "dashboard_controller"
puts self.class.name
# => "SoapTest::DashboardController"
end
end
There are several String inflection methods you can call on the #name to get the formatted version.
However, I strongly encourage you to use a namespaced main controller.
Instead of using
class SoapTest::DashboardController < ApplicationController
you can extend a SoapTest::ApplicationController
class SoapTest::ApplicationController < ApplicationController
before_filter :check_company_features
def check_company_features
# ...
end
end
class SoapTest::DashboardController < SoapTest::ApplicationController
end

rails: methods from module included in controller not available in view

Strange thing – I have Authentication module in lib/ like this:
module Authentication
protected
def current_user
User.find(1)
end
end
and in ApplicationController I'm including this module and all helpers, but method current_user is available in controllers, but not from views :( How can I make this work?
If the method were defined directly in the controller, you'd have to make it available to views by calling helper_method :method_name.
class ApplicationController < ActionController::Base
def current_user
# ...
end
helper_method :current_user
end
With a module, you can do the same, but it's a bit more tricky.
module Authentication
def current_user
# ...
end
def self.included m
return unless m < ActionController::Base
m.helper_method :current_user # , :any_other_helper_methods
end
end
class ApplicationController < ActionController::Base
include Authentication
end
Ah, yes, if your module is meant to be strictly a helper module, you can do as Lichtamberg said. But then again, you could just name it AuthenticationHelper and put it in the app/helpers folder.
Although, by my own experience with authentication code, you will want to have it be available to both the controller and views. Because generally you'll handle authorization in the controller. Helpers are exclusively available to the view. (I believe them to be originally intended as shorthands for complex html constructs.)
Did you declare it with
helper :foo # => requires 'foo_helper' and includes FooHelper
helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper
in your ApplicationController?
http://railsapi.com/doc/rails-v2.3.3.1/classes/ActionController/Helpers/ClassMethods.html#M001904

Resources