How to Define a 'Before_Save' Callback in a Module

Is it possible to define a 'before_save' callback in a module?

In Ruby on Rails < 3 (without Rails features, only Ruby)

module MyModule
def self.included(base)
base.class_eval do
before_save :do_something
end
end

def do_something
#do whatever
end
end

In Ruby on Rails >= 3 (with Rails Concern feature)

module MyModule
extend ActiveSupport::Concern

included do
before_save :do_something
end

def do_something
#do whatever
end
end

rails before_save callback does not trigger

It's because if amount is greater than 9999, the value returned from block is false (from self.confirmed = false line) - and if block (or method) passed into before_save returns false, ActiveRecord stops saving the record. So the simple solution is to add true that would be returned:

before_save do
if self.amount > 9999
self.confirmed = false
else
self.confirmed = true
end
true
end

The relevant piece of documentation for reference:

If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last.

http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#module-ActiveRecord::Callbacks-label-Canceling+callbacks

Implement Callbacks in Loopback 'Before Save' hook

You should implement a promise in your external function, then wait for the external API call and return the response using the resolve callback.

module.exports = function(Blockanchor) {

Blockanchor.observe('before save', function GetHash(ctx, next) {
if (ctx.instance) {
//console.log('ctx.instance', ctx.instance)
var theHash = ctx.instance.hash; //NB - This variable is required for the external API call to get the relevant data

//Run below functions to call an external API
//Invoke function
ExternalFunction(theHash).then(function(externalData){
ctx.instance.somedata = externalData;

next();
})
} else {
next();
}
}); //Blockanchor.observe

}//module.exports = function(Blockanchor)

function ExternalFunction(theHash){
return new Promise(function(resolve, reject){
var externalData = 'Foo'
resolve(externalData)
})
}

Run before_save after changed in attribute in a related model

Instead of defining an after save callback on the Airplane model, define a after_add callback on the payments association.

class Airplane < ActiveRecord::Base
has_many :payments, after_add: :checks_if_everything_has_been_paid

def checks_if_everything_has_been_paid
# work some magic
end
end

Update: I think the following may be a better approach if I understand your data model correctly. If a payment or installment is saved it will trigger the airplane to check for full payment:

class Airplane < ActiveRecord::Base
has_many :payments
has_many :installments, through: :payments

def check_for_full_payment
# work some magic
end
end

class Payment < ActiveRecord::Base
belongs_to :airplane
has_many :installments

after_save :trigger_full_payment_check

def trigger_payments_check
airplane.check_for_full_payment
end
end

class Installment < ActiveRecord::Base
belongs_to :payment

delegate :airplane, to: :payment

after_save :trigger_full_payment_check

def trigger_payments_check
airplane.check_for_full_payment
end
end

The nice thing about this approach is that the logic in Payment and Installment is identical, so you can extract it to a module:

module TriggerFullPaymentCheck
def self.included(base)
base.after_save :trigger_full_payment_check
end

def trigger_payments_check
airplane.check_for_full_payment
end
end

class Airplane < ActiveRecord::Base
has_many :payments
has_many :installments, through: :payments

def check_for_full_payment
# work some magic
end
end

class Payment < ActiveRecord::Base
include TriggerFullPaymentCheck

belongs_to :airplane
has_many :installments
end

class Installment < ActiveRecord::Base
include TriggerFullPaymentCheck

belongs_to :payment
delegate :airplane, to: :payment
end

Ruby on Rails - Apply function to multiple fields using before_save callback?

Perhaps something like this:

ATTRS_TO_DOWNCASE = %i( email first_name last_name ) 
before_save :downcase_attrs

private
def downcase_attrs
ATTRS_TO_DOWNCASE.each do |attr|
write_attribute(attr, read_attribute(attr).downcase)
end
end

Better way to write before_save function to check all targeted attributes not blank?

I assume that the .except method was used because id, created_at, and updated_at are all internally generated and managed by MySQL. So, it would be unusual for that list to expand or change. I agree the code is good as provided. If you wanted to shorten it at all, you could use a ternary as in:

def check_completed
self.attributes.except("id", "created_at", "updated_at").all? {|k, v| v.present?} ? 1 : 0
end

Eliminating the .except method exposes you to managing this method any time the model changes.

Extending this discussion:
I am curious about your desire to return 1 or 0? Not seeing more code, I am not sure of your intentions. However, if a "before" callback returns false, execution is stopped and the transaction is rolled back. In any other case, execution continues. In Ruby, 0 is not false. False is only triggered by either false or nil. My expectation would be that it would be more likely to use true in place of 1 and false in place of 0? if so, the code would be:

def check_completed
self.attributes.except("id", "created_at", "updated_at").all? {|k, v| v.present?} ? true : false
end

Such that if any user attribute was not present, the transaction would be canceled and rolled back. but, that is up to you.

How can I test that my before_save callback does the right thing

Mocha is the way to go. I'm not familiar with rspec, but this is how you would do it in test unit:


def test_google_api_gets_called_for_user_and_accoc_user
user = mock('User') # define a mock object and label it 'User'
accoc_user = mock('AssocUser') # define a mock object and label it 'AssocUser'

# instantiate the model you're testing with the mock objects
model = Model.new(user, assoc_user)

# stub out the other_user method. It will return cuser1 when the mock user is
# passed in and cuser2 when the mock assoc_user is passed in
cuser1 = mock('Cuser1')
cuser2 = mock('Cuser2')
model.expects(:other_user).with(user).returns(cuser1)
model.expects(:other_user).with(assoc_user).returns(cuser2)

# set the expectations on the Google API
api1 - mock('GoogleApiUser1') # define a mock object and lable it 'GoogleApiUser1'
api2 - mock('GoogleApiUser2') # define a mock object and lable it 'GoogleApiUser2'

# call new on Google passing in the mock user and getting a mock Google api object back
Google.expects(:new).with(:user => cuser1).returns(api1)
api1.expects(:sync_user).with(cuser1)

Google.expects(:new).with(:user => cuser2).returns(api2)
api2.expects(:sync_user).with(cuser2)

# now execute the code which should satisfy all the expectations above
model.save!
end

The above may seem complicated, but it's not once you get the hang of it. You're testing that when you call save, your model does what it is supposed to do, but you don't have the hassle, or time expense of really talking to APIs, instantiating database records, etc.

Stop callback chain and send notification beforeSave method ApostropheCMS

In your server-side code:

    self.beforeSave = function(req, piece, options, callback) {
let success = true;
if (Array.isArray(piece._subevents) && piece._subevents.length) {
success = self.checkDateAndTimeCompabilitiyWithChildren(piece);
}
if (!success) {
return callback('incompatible');
}
return callback(null);
};

And on the browser side:

// in lib/modules/my-pieces-module/public/js/editor-modal.js
apos.define('my-pieces-module-editor-modal', {
extend: 'apostrophe-pieces-editor-modal',
construct: function(self, options) {
self.getErrorMessage = function(err) {
if (err === 'incompatible') {
return 'A message suitable for this case.';
} else {
return 'A generic error message.';
}
};
}
});

If the error reported by the callback is a string, it is passed to the browser. The browser can then recognize that case and handle it specially. 'my-pieces-module-editor-modal' should be substituted with the name of your pieces module followed by -editor-modal.



Related Topics



Leave a reply



Submit