after_initialize never invoked in Rails 3 via mixin - ruby-on-rails

I am using a mixin to add some functionality to my model (Person). In the mixin I need some initializations to be done so I am trying to use the "after_initialize" callback macro to invoke an initialization method. The model (Person) is only a base class for some other models.
The problem I am having is that it gets never called. I tried to debug it but the breakpoint never got hit. Also logging gives me no output.
I couldn`t find any help (as this construct should be working in Rails 3 according to the Api docs and some posts here).
/lib/mymodule.rb
module MyModule
after_initialize :generate_ids
def generate_ids
logger.info "invoked" #never hit
end
end
/models/person.rb
require "mymodule"
class Person < ActiveRecord::Base
include MyModule
end
/models/customer.rb
class Customer < Person
# nothing so far
end
*/controllers/customers_controller.rb (action => new)*
# GET /customers/new
# GET /customers/new.json
def new
#person = Customer.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #customer }
end
end
Please be indulgent to me as I am a "newbie" to RoR.
Thank you very much !
Best regards,
Thomas
UPDATE
After restarting the local app server it gives me the following exception:
ActionController::RoutingError (undefined method `after_initialize' for SequentialRecord:Module):
I assume this callback can`t be used in mixins ?

Try something like that (not tested).
module MyModule
def self.included(base)
base.after_initialize :generate_ids
end
def generate_ids
logger.info "invoked" #never hit
end
end

Related

Rails decorator method not being called in production, works in development

I add the following override for a controller that I inherit from a gem (Spree):
module Spree
module Admin
UsersController.class_eval do
def index
if params[:role].present?
included_users = Spree::User.joins(:role_users).
where( spree_role_users: { role_id: params[:role] } ).map(&:id)
flash[:notice] = "Filtered in #{included_users.count} users"
#users = #users.where(id: included_users)
end
end
end
end
end
Basically, it filters by an additional parameter on the Admin::UsersController controller. The source for that controller in the gem doesn't actually define the index method, so mine just gets called instead.
Now, this works perfectly well in development. However, in production, this method never gets called.
Is there something about class_eval that I'm not getting here? Shouldn't things like this work basically the same in production as they do in development?
Thanks for any help.
Decorators are objects that wrap another object. For example they are often used to wrap models with presentational logic.
class UserDecorator < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
end
> #user = UserDecorator.new(User.new(first_name: 'John', last_name: 'Doe'))
> #user.full_name
=> "John Doe"
This is not a decorator method - you are just reopening the class and adding a method. This is known as monkey-patching.
Using class_eval in this case exactly the same in this as using the class keyword:
module Spree
module Admin
class UsersController
def index
if params[:role].present?
included_users = Spree::User.joins(:role_users).
where( spree_role_users: { role_id: params[:role] } ).map(&:id)
flash[:notice] = "Filtered in #{included_users.count} users"
#users = #users.where(id: included_users)
end
end
end
end
end
With monkey-patches the key is ensuring that your class redefinition is read. I'm guessing the difference between dev and production is due to class catching which prevents the class from being read from /app if it has already been defined by the Spree gem. config/application.rb uses bundler to require all the gems when the application starts up.
You can assure that a monkey-patch is loaded by placing it in config/initializers as all files in that directory are loaded on startup.
But a better alternative to monkeypatching may be to instead subclass the vendor controller and route to it:
class MyUsersController < ::Spree::Admin::UsersController
def index
if params[:role].present?
included_users = Spree::User.joins(:role_users).
where( spree_role_users: { role_id: params[:role] } ).map(&:id)
flash[:notice] = "Filtered in #{included_users.count} users"
#users = #users.where(id: included_users)
end
end
end
see also:
3 Ways to Monkey-Patch Without Making a Mess

How can I redirect from a Module?

I tried TO Google Can I redirect_to in rails modules but couldn't come up with anything. Basically, I have a method that I am going to use across a couple of Controllers.
lib/route_module.rb
module RouteModule
def self.user_has_active_chocolate(c_id, u_id)
chocolate_id = c_id
user_id = u_id
unless UserChocolate.where(id: chocolate_id).empty?
if UserChocolate.where(id: chocolate_id).last.active?
true
else
false
# BREAKS OVER HERE...
redirect_to "/user/new-chocolate/#{user_id}"
end
else
false
redirect_to "/admin"
end
end
end
app/controllers/user_controllers.rb
include RouteModule
before_filter :thingz, only: [:display_user_chocolate]
# private
def thingz
RouteModule.user_has_active_chocolate(params["chocolate_id"], params["user_id"])
end
But... whenever I run this... It will break as soon as it hit's redirect_to.
undefined method `redirect_to' for RouteModule:Module
My other option is use ActiveSupport::Concerns but I just trouble converting this Module into a Concern.
When you include a module, it acts as a mixin. That said, you include and get all the methods of the module in the context of your class. The proper way would be:
module RouteModule
def user_has_active_chocolate(c_id, u_id) # NO self
...
end
end
And in the class:
include RouteModule
def thingz
# NO module method call (NO RouteModule)
user_has_active_chocolate(params["chocolate_id"], params["user_id"])
end

Rails shared controller actions

I am having trouble building a controller concern. I would like the concern to extend the classes available actions.
Given I have the controller 'SamplesController'
class SamplesController < ApplicationController
include Searchable
perform_search_on(Sample, handle: [ClothingType, Company, Collection, Color])
end
I include the module 'Searchable'
module Searchable
extend ActiveSupport::Concern
module ClassMethods
def perform_search_on(klass, associations = {})
.............
end
def filter
respond_to do |format|
format.json { render 'api/search/filters.json' }
end
end
end
end
and, despite setting up a route i get the error 'The action 'filter' could not be found for SamplesController'.
I thought it might be to do with wether I include, or extend the module. I tried using extend but that also gave the same error.
I still need to be able to feed the module some configuration options on a per controller basis. Is it possible to achieve what I am trying to do here?
Any help appreciated, thanks
You should pass actions to the included block and perform_search_on to the class_methods block.
module Searchable
extend ActiveSupport::Concern
class_methods do
def perform_search_on(klass, associations = {})
.............
end
end
included do
def filter
respond_to do |format|
format.json { render 'api/search/filters.json' }
end
end
end
end
When your Searchable module include a method perform_search_on and the filter action.
Try removing the methods from the module ClassMethods. That is making them instance methods.

Changing respond_to url for an ActiveModel object

Summary:
How do I customize the path that respond_to generates for an ActiveModel object?
Update: I'm looking for a hook, method override, or configuration change to accomplish this, not a workaround. (A workaround is easy but not elegant.)
Context & Example:
Here is an example to illustrate. I have a model, Contract, which has a lot of fields:
class Contract < ActiveRecord::Base
# cumbersome, too much for a UI form
end
To make the UI code easier to work with, I have a simpler class, SimpleContract:
class SimpleContract
include ActiveModel::Model
# ...
def contract_attributes
# convert SimpleContract attributes to Contract attributes
end
def save
Contract.new(contract_attributes).save
end
end
This works well, but I have a problem in my controller...
class ContractsController < ApplicationController
# ...
def create
#contract = SimpleContract.new(contract_params)
flash[:notice] = "Created Contract." if #contract.save
respond_with(#contract)
end
# ...
end
The problem is that respond_with points to simple_contract_url, but I want it to point to contract_url instead. What is the best way to do that? (Please note that I'm using ActiveModel.)
(Note: I'm using Rails 4 Beta, but that isn't central to my problem. I think a good answer for Rails 3 will work as well.)
Sidebar: if this approach to wrapping a model in a lightweight ActiveModel class seem unwise to you, please let me know in the comments. Personally, I like it because it keeps my original model simple. The 'wrapper' model handles some UI particulars, which are intentionally simplified and give reasonable defaults.
First, here is an answer that works:
class SimpleContract
include ActiveModel::Model
def self.model_name
ActiveModel::Name.new(self, nil, "Contract")
end
end
I adapted this answer from kinopyo's answer to Change input name of model.
Now, for the why. The call stack of respond_to is somewhat involved.
# Start with `respond_with` in `ActionController`. Here is part of it:
def respond_with(*resources, &block)
# ...
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
# That takes us to `call` in `ActionController:Responder`:
def self.call(*args)
new(*args).respond
end
# Now, to `respond` (still in `ActionController:Responder`):
def respond
method = "to_#{format}"
respond_to?(method) ? send(method) : to_format
end
# Then to `to_html` (still in `ActionController:Responder`):
def to_html
default_render
rescue ActionView::MissingTemplate => e
navigation_behavior(e)
end
# Then to `default_render`:
def default_render
if #default_response
#default_response.call(options)
else
controller.default_render(options)
end
end
And that is as far as I've gotten for the time being. I have not actually found where the URL gets constructed. I know that it happens based on model_name, but I have not yet found the line of code where it happens.
I'm not sure that I fully understand your problem, but could you do something like this?
class SimpleContract
include ActiveModel::Model
attr_accessor :contract
# ...
def contract_attributes
# convert SimpleContract attributes to Contract attributes
end
def save
self.contract = Contract.new(contract_attributes)
contract.save
end
end
-
class ContractsController < ApplicationController
# ...
def create
#simple_contract = SimpleContract.new(contract_params)
flash[:notice] = "Created Contract." if #simple_contract.save
respond_with(#simple_contract.contract)
end
# ...
end
I may be way off base. Hopefully that at least triggers an idea for you.

rails 3: how to abort delivery method in actionmailer?

In my mailer controller, under certain conditions (missing data) we abort sending the email.
How do I exit the controller method without still rendering a view in that case?
return if #some_email_data.nil?
Doesn't do the trick since the view is still rendered (throwing an error every place I try to use #some_email_data unless I add a lot of nil checks)
And even if I do the nil checks, it complains there's no 'sender' (because I supposed did a 'return' before getting to the line where I set the sender and subject.
Neither does render ... return
Basically, RETURN DOESN'T RETURN inside a mailer method!
A much simpler solution than the accepted answer would be something like:
class SomeMailer < ActionMailer::Base
def some_method
if #some_email_data.nil?
self.message.perform_deliveries = false
else
mail(...)
end
end
end
If you're using Rails 3.2.9 (or later things even better) - there you can finally conditionally call mail(). Here's the related GitHub thread. Now the code can be reworked like this:
class SomeMailer < ActionMailer::Base
def some_method
unless #some_email_data.nil?
mail(...)
end
end
end
I just encountered same thing here.
My solution was following:
module BulletproofMailer
class BlackholeMailMessage < Mail::Message
def self.deliver
false
end
end
class AbortDeliveryError < StandardError
end
class Base < ActionMailer::Base
def abort_delivery
raise AbortDeliveryError
end
def process(*args)
begin
super *args
rescue AbortDeliveryError
self.message = BulletproofMailer::BlackholeMailMessage
end
end
end
end
Using these wrapper mailer would look like this:
class EventMailer < BulletproofMailer::Base
include Resque::Mailer
def event_created(event_id)
begin
#event = CalendarEvent.find(event_id)
rescue ActiveRecord::RecordNotFound
abort_delivery
end
end
end
It is also posted in my blog.
I've found this method that seems the least-invasive, as it works across all mailer methods without requiring you to remember to catch an error. In our case, we just want a setting to completely disable mailers for certain environments. Tested in Rails 6, although I'm sure it'll work just fine in Rails 5 as well, maybe lower.
class ApplicationMailer < ActionMailer::Base
class AbortDeliveryError < StandardError; end
before_action :ensure_notifications_enabled
rescue_from AbortDeliveryError, with: -> {}
def ensure_notifications_enabled
raise AbortDeliveryError.new unless <your_condition>
end
...
end
The empty lambda causes Rails 6 to just return an ActionMailer::Base::NullMail instance, which doesn't get delivered (same as if your mailer method didn't call mail, or returned prematurely).
Setting self.message.perform_deliveries = false did not work for me.
I used a similar approach as some of the other answers - using error handling to control the flow and prevent the mail from being sent.
The example below is aborting mail from being sent in non-Production ENVs to non-whitelisted emails, but the helper method logic can be whatever you need for your scenario.
class BaseMailer < ActionMailer::Base
class AbortedMailer < StandardError; end
def mail(**args)
whitelist_mail_delivery(args[:to])
super(args)
rescue AbortedMailer
Rails.logger.info "Mail aborted! We do not send emails to external email accounts outside of Production ENV"
end
private
def whitelist_mail_delivery(to_email)
return if Rails.env.production?
raise AbortedMailer.new unless internal_email?(to_email)
end
def internal_email?(to_email)
to_email.include?('#widgetbusiness.com')
end
end
I just clear the #to field and return, so deliver aborts when it doesn't have anything there. (Or just return before setting #to).
I haven't spent much time with rails 3 but you could try using
redirect_to some_other_route
alternatively, if you're really just checking for missing data you could do a js validation of the form fields and only submit if it passes.

Resources