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.
Related
I'm using mongoid for an app where the user is the parent document, and pretty much all other information is embedded in the user. So for instance, my controller #new action for a Relationship belonging to the user looks something like:
def new
#relationship = current_user.relationships.new(friend_id: params[:fid])
#relationship.validate
end
Because I run validations on the relationship that will show up in the view and some of those validations need to be able to reference the parent, I can't just call #relationship = Relationship.new(friend_id: params[:fid]), but having instantiated this relationship in the user's relationship array, it's now hanging out in there, even if the user decides they don't want to make a new relationship after all and they go to another part of the site. If they go to the relationship index page, they'll see it in the list unless I filter it out.
If the relationship is valid and they do something elsewhere that causes the user to save, that dummy relationship is now a real one. If it's not valid, the save is going to fail for unknown reasons.
I have a number of models I intend to embed in the user, so I will have this issue with every one of them.
I know I can call current_user.reload to clear the junk out, but it feels ridiculous to me that I would have to hit the database every time I wanted to do this. I could also orphan the relationship after validating, but that feels hacky.
It seems to me that this is a problem people should run into all the time with embedded documents, so I would think there'd be some kind of built in solution, but I can't seem to find it anywhere. I saw this question, which is similar to mine, but I want something more extensible, so that I don't have to put it everywhere.
I'm about to make a module that will add a clear_unsaved_#{relation} method to the class for each embedded relation, but the idea frustrates me, so I wanted to see if anyone has a better idea of how to do it, and also where is best to call it.
I ended up making a monkey patch that overrides Mongoid's embeds_many and embeds_one class methods to also define an instance method for clearing unsaved documents for that relation. This felt like the most straightforward way to me because it's very little code and it means I don't have to remember to include it places.
# config/initializers/patches/dirty_tracking_embedded.rb
module DirtyTrackingEmbedded
# override the embedding methods to also make dirty-tracking
def embeds_many(name, options= {}, &block)
define_method "clear_unsaved_#{name}" do
# remove_child removes it from the array without hitting the database
send(name).each {|o| remove_child(o) unless o.persisted?}
end
super
end
def embeds_one(name, options={}, &block)
define_method "clear_unsaved_#{name}" do
dirty = send(name)
remove_child(dirty) unless dirty.persisted?
end
super
end
end
module Mongoid
module Association
module Macros
module ClassMethods
prepend DirtyTrackingEmbedded
end
end
end
end
Then in my controller I resorted to an after_action:
# app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
after_action :clear_unsaved, only: [:new]
def new
#relationship = current_user.relationships.new(friend_id: params[:fid])
#relationship.validate
end
private
def clear_unsaved
current_user.clear_unsaved_relationships
end
end
Other Possibilities
Different monkey patch
You could monkey patch the setup_instance_methods! methods in Mongoid::Association::Embedded::EmbedsMany and Mongoid::Association::Embedded::EmbedsOne to include setting up an instance method to clear unsaved. You can find an example of how the Mongoid folks do that sort of thing by looking at Mongoid::Association::Accessors#self.define_ids_setter!. I'd recommend doing your patching with a prepend like in the solution I went with, so you can inherit the rest of the method.
Combo monkey patch and inheritance
Mongoid chooses which class to use to instantiate an association from a constant called MACRO_MAPPING in Mongoid::Association, so you could make classes that inherit from EmbedsMany and EmbedsOne with just setup_instance_methods! overridden to add the needed instance method, then you would only have to monkey patch MACRO_MAPPING to map to your new classes.
Concern
If you're anti-monkey patching, you could use the code from my DirtyTrackingEmbedded module to make an ActiveSupport::Concern that does the same thing. You'll want to put the overridden methods in the class_methods block, and then just make sure you include this module after you include Mongoid::Document in any model class you want it in.
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
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.
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
I am in the middle of migrating my application from using subdirectories for userspace to subdomains (ie. domain.com/~user to user.domain.com). I've got a method in my user class currently to get the "home" URL for each user:
class User
def home_url
"~#{self.username}"
# How I'd like to do it for subdomains:
#"http://#{self.username}.#{SubdomainFu.host_without_subdomain(request.host)}"
end
end
I'd like to update this for subdomains, but without hardcoding the domain into the method. As you can see, I am using the subdomain-fu plugin, which provides some methods that I could use to do this, except that they need access to request, which is not available to the model.
I know it's considered bad form to make request available in a model, so I'd like to avoid doing that, but I'm not sure if there's a good way to do this. I could pass the domain along every time the model is initialized, I guess, but I don't think this is a good solution, because I'd have to remember to do so every time a class is initialized, which happens often.
The model shouldn't know about the request, you're right. I would do something like this:
# app/models/user.rb
class User
def home_url(domain)
"http://#{username}.#{domain}"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def domain
SubdomainFu.host_without_subdomain(request.host)
end
# Make domain available to all views too
helper_method :domain
end
# where you need it (controller or view)
user.home_url(domain)
If there is such a thing as a canonical user home URL, I would make a configurable default domain (e.g. YourApp.domain) that you can use if you call User#home_url without arguments. This allows you to construct a home URL in places where, conceptually, the "current domain" does not exist.
While molf's answer is good, it did not solve my specific problem as there were some instances where other models needed to call User#home_url, and so there would be a lot of methods I'd have to update in order to pass along the domain.
Instead, I took inspiration from his last paragraph and added a base_domain variable to my app's config class, which is the set in a before_filter in ApplicationController:
module App
class << self
attr_accessor :base_domain
end
end
class ApplicationController < ActionController::Base
before_filter :set_base_domain
def set_base_domain
App.base_domain = SubdomainFu.host_without_subdomain(request.host)
end
end
And thus, when I need to get the domain in a model, I can just use App.base_domain.