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
The Command Rbenv Install Is Missing
Ruby Working on Array Elements in Groups of Four
How Would You Test Observers with Rspec in a Ruby on Rails Application
How to Redefine a Ruby Constant Without Warning
Rails 3 - Restricting Formats for Action in Resource Routes
Difference Between Map and Each
Installing Rails: "File Not Found: Lib"
How to Write Negative Loop in Ruby Like For(I=Index; I >= 0; I --)
What Is the &: of &:Afunction Doing
Passing a Hash to a Function ( *Args ) and Its Meaning
When to Use Struct Instead of Hash in Ruby
Generate Unique Random String with Letters and Numbers in Lower Case
Relative Path to Your Project Directory
What Is the Use of "#!/Usr/Local/Bin/Ruby -W" at the Start of a Ruby Program