Devise: remove module for extended model - ruby-on-rails

I got big legacy app with model User. I added new type of user:
class ExtendedUser < User
devise :database_authenticatable, ...
end
New type of user is extending existing one, so all code written for User should work also for ExtendedUser, without extending it is impossible.
Question is how to remove devise module from ExtendedUser which comes from User?
User is confirmable and ExtendedUser should not be.
I'm suspecting that removing module is impossible and easiest way is to put .skip_confirmation! before all places in code where this user will be created and where email can change.

You could just use inheritance to dynamically evaluate which modules are included:
class User < User
devise *devise_modules
# ...
def self.devise_modules
[:database_authenticatable, ...]
end
end
class ExtendedUser < User
# ...
def self.devise_modules
super.excluding(:confirmable)
end
end

Related

Ruby (rails) conflicting class and modules names when nested

I have a basic User model in app/models/user.rb. I also have a few services in lib. For example, I have lib/services/user/creation_service.rb. The following code generates an error:
# lib/services/user/creation_service.rb
module Services
module User
class CreationService
...
def create_new_user
# User.new below causes an error because it defaults to Services::User which is a module instead of User which is an ActiveRecord class
User.new
...
end
...
end
end
end
Is there any way to get User.new to refer to app/models/user.rb instead of the Services::User module in the code above?
Any help would be greatly appreciated!
Use ::User.new instead of User.new. The double colon basically tells ruby to look for the constant that does not have a parent.
So in this case ::User will point to User which is a class instead of Services::User which is a module.

Two modules with same method names included in same class

I am working with ruby on rails and I am basically trying to include two modules into the same model/class with both modules having the same method names. An example will be demonstrated below, however my questions are:
Is there a way to include module conditionally? or
Is there a way to invoke based on the specific instance of the class.
An example is a simple complete profile wizard.
How its suppose to work
Case 1: If the user is lets say a Transporter, step_one is completed when the user has a company_name is present.
Case 2: On the otherhand if the user is a Client, step_one is completed when the user has a telephone present.
class User < ApplicationRecord
include ClientWizard
include TransporterWizard
end
module ClientWizard
def step_one_completed?
self.name.present?
end
end
module TransporterWizard
def step_one_completed?
self.company_name.present?
end
end
No, module methods all exist within the class's namespace. Consequently, this doesn't seem like a particularly good use case for modules.
You could give the methods module-specific names (client_wizard_step_one_completed?), but I'd recommend instead defining the wizards as separate classes, and passing the user instance as a parameter.
class User < ApplicationRecord
def client_wizard
ClientWizard.new(self)
end
end
class ClientWizard
def initialize(user)
#user = user
end
def step_one_completed?
#user.name.present?
end
end

Rails 5: Calling a service class - relation mapping

I am trying to learn how to write a service class in my rails 5 app.
When a user registers with devise, I'm trying to incorporate a service class that makes models associated with the user's account on creation of the user account.
In my devise registrations controller, I have:
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_permitted_parameters, if: :devise_controller?
def create
super do |user|
if user.persisted?
User::CompleteRegistration.call(user: user)
end
end
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :email])
devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name, :email, ])
end
private
end
In my app / services/ user folder, I have a file called: complete_registration.rb:
class User::CompleteRegistration #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
Profile::SetupService.call(user: user)
Setting::SetupService.call(user: user)
User::OrganisationMapperService.new(user: user).call
end
# def join_an_organisation
# render "organisation_requests/new"
# end
private
attr_accessor :user
end
When I try to save this and then sign up as a user, I can see an error message that says:
NameError - uninitialized constant User::CompleteRegistrations:
I note that it has pluralised the word 'registration'. I don't know if this has something to do with the problem. I tried saving the file as a plural, and renaming the class as a plural, but that didn't work.
The actual error rendered says:
PG::UndefinedTable at /users
ERROR: relation "complete_registration_services" does not exist
LINE 8: WHERE a.attrelid = '"complete_registration_se...
I cant expand the message to get more detail, but there is no relation to be found, because it isnt an active record table that I'm trying to call.
I also tried adding a callback to my user model as an alternative solution:
class User < ApplicationRecord
after_update :after_confirmation_setup
def after_confirmation_setup
return unless !self.confirmed_at.blank?
User::CompleteRegistration.call(user: #user)
end
But that doesnt work either.
Can anyone see how I can setup my app to call a service class on user create (from the registration controller, or any other method)?
By the looks of things, this seems like a naming issue.
From first appearances (I can't say definatively without actually using your code base in console and debugging) but based on a quick scan and looking at the errors you are receiving this appears to be one of Ruby's quirks which I have bumped into a few times.
You have a User class in the global scope (In your models directory), and you have a service class User::CompleteRegistration, which when you call from somewhere is perfectly logical to assume will point to the defined User::CompleteRegistration class. However, Ruby doesn't see it like that.
User::CompleteRegistration can be split into two parts, first User:: Is evaluated, and Ruby searches for a User class, and grabs your model. It then evaluates the CompleteRegistration, and looks for a defined CompleteRegistration that can be used in your User class. This is may be why you are getting the error about a relation. It is searching for a relation within the User model scope.
So essentially, when you write User::CompleteRegistration Ruby doesnt say Right! lets grab a User::CompleteRegistration class! Ruby says Right! lets grab a CompleteRegistration class that can be used within the User scope!
To prevent this, I would perhaps change the naming of your service to something more simple, like: Registration::Complete and avoid any overlapping of class / module names within the global scope.
also on a side note, I too love using services in code, and I don't really want to write a shameless plug on SO but perhaps it your case it can help? I have written a gem to provide easy to use services that has an implementation not too dissimilar to what you are using, perhaps it could be of use, whether you use it as a gem or just scan the code in it, I hope somehow maybe it can help.

Why do functions from my Rails plugin not work without specifically requiring?

I need some help with my plugin. I want to extend ActiveRecord::Base with a method that initializes another method that can be called in the controller.
It will look like this:
class Article < ActiveRecord::Base
robot_catch :title, :text
...
end
My attempt at extending the ActiveRecord::Base class with robot_catch method looks like following. The function will initialize the specified attributes (in this case :title and :text) in a variable and use class_eval to make the robot? function available for the user to call it in the controller:
module Plugin
module Base
extend ActiveSupport::Concern
module ClassMethods
def robot_catch(*attr)
##robot_params = attr
self.class_eval do
def robot?(params_hash)
# Input is the params hash, and this function
# will check if the some hashed attributes in this hash
# correspond to the attribute values as expected,
# and return true or false.
end
end
end
end
end
end
ActiveRecord::Base.send :include, Plugin::Base
So, in the controller, this could be done:
class ArticlesController < ApplicationController
...
def create
#article = Article.new(params[:article])
if #article.robot? params
# Do not save this in database, but render
# the page as if it would have succeeded
...
end
end
end
My question is whether if I am right that robot_catch is class method. This function is to be called inside a model, as shown above. I wonder if I am extending the ActiveRecord::Base the right way. The robot? function is an instance method without any doubt.
I am using Rails 3.2.22 and I installed this plugin as a gem in another project where I want to use this functionality.
Right now, it only works if I specifically require the gem in the model. However, I want it the functionality to be included as a part of ActiveRecord::Base without requiring it, otherwise I'd have to require it in every model I want to use it, not particularly DRY. Shouldn't the gem be automatically loaded into the project on Rails start-up?
EDIT: Maybe callbacks (http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html) would be a solution to this problem, but I do not know how to use it. It seems a bit obscure.
First, I would suggest you make sure that none of the many many built in Rails validators meet your needs.
Then if that's the case, what you actually want is a custom validator.
Building a custom validator is not as simple as it might seem, the basic class you'll build will have this structure:
class SpecialValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Fill this with your validation logic
# Add to record.errors if validation fails
end
end
Then in your model:
class Article < ActiveRecord::Base
validates :title, :text, special: true
end
I would strongly suggest making sure what you want is not already built, chances are it is. Then use resources like this or ruby guides resources to continue going down the custom validator route.
Answer
I found out the solution myself. Bundler will not autoload dependencies from a gemspec that my project uses, so I had to require all third party gems in an engine.rb file in the lib/ directory of my app in order to load the gems. Now everything is working as it should.
Second: the robot_catch method is a class method.

How to get the current instance module in Ruby

That title doesn't really explain everything so here goes. I have two Rails engines that share some functionality (ie. user model and authentication). I have a base User class and then two other User classes that inherit from this base class for each app like so:
class User; end
class App1::User < ::User; end
class App2::User < ::User; end
My authentication has a method similar to the following
def user_from_session
User.find_by_id(session[:user_id])
end
which is included in my application_controller. My problem here is that when a user is fetched... it always uses the base User class. What I really want is to be able to fetch a User that is the same type as the app calling that method.
For instance, if a user is on SomeController:
class App1::SomeController < ApplicationController; end
I want the method in the application_controller to pull out the App1 so that it instantiates an App1::User rather than just a User
Is this possible?
I'm NOT looking for a solution that involves two user_from_session methods, one for each application. I am aware of how to implement that. I'm more interested in know if this type of thing is possible in Ruby.
Though I'd caution you to find a better, less hacky way to do this, here's how you might do it:
def user_from_session
# App1::Whatever::FooController -> App1::Whatever
module_name = self.class.name.split('::')[0..-2].join('::')
# App1::Whatever -> App1::Whatever::User
user_class = "#{module_name}::User".constantize
# App1::Whatever::User.find_by_id(...)
user_class.find_by_id(session[:user_id])
end

Resources