Modifying ActiveRecord models before preventing deletion - ruby-on-rails

Some records in my application have a DOI assigned to them and in that case they should not be deleted. Instead, they should have their description changed and be flagged when a user triggers their deletion. A way to do this, I thought, would be as follows in the relevant model:
before_destroy :destroy_validation
private
def destroy_validation
if metadata['doi'].blank?
# Delete as normal...
nil
else
# This is a JSON field.
modified_metadata = Marshal.load(Marshal.dump(metadata))
description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
modified_metadata['description'] = description
modified_metadata['tombstone'] = true
update_column :metadata, modified_metadata
raise ActiveRecord::RecordNotDestroyed, 'Records with DOIs cannot be deleted'
end
end
This does indeed prevent deletion, but the record appears unchanged afterwards rather than having a modified description. Here's an example of a test:
test "records with dois are not deleted" do
record = Record.new(metadata: metadata)
record.metadata['doi'] = 'this_is_a_doi'
assert record.save
assert_raises(ActiveRecord::RecordNotDestroyed) { record.destroy! }
assert Record.exists?(record.id)
modified_record = Record.find(record.id)
puts "#{record.description}" # This is correctly modified as per the callback code.
puts "#{modified_record.description}" # This is the same as when the record was created.
end
I can only guess that Rails is rolling back the update_column due to an exception having been raised, though I may be mistaken. Is there anything I can do to prevent this?

save and destroy are automatically wrapped in a transaction
https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
So destroy fails, transactions is rolled back and you can't see updated column in tests.
You could try with after_rollback callback https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_rollback
or do record.destroy check for record.errors, if found update record with method manually record.update_doi if record.errors.any?.
before_destroy :destroyable?
...
def destroyable?
unless metadata['doi'].blank?
errors.add('Doi is not empty.')
throw :abort
end
end
def update_doi
modified_metadata = Marshal.load(Marshal.dump(metadata))
description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
modified_metadata['description'] = description
modified_metadata['tombstone'] = true
update_column :metadata, modified_metadata
end
Tip: use record.reload instead of Record.find(record.id).

Related

Increment field within validator

I have a custom validator that checks if the user has entered the correct SMS code. When the user enters the wrong code I need to log the failed attempt and limit their retries to 3 per code.
I have created the following validator that works however the field is not being incremented.
def token_match
if token != User.find(user_id).verification_token
User.find(user_id).increment!(:verification_fails)
errors.add(:sms_code, "does not match")
end
end
The problem is as soon as I add the error the previous statement is rolled back. If I comment out the errors.add line then the increment works however there is no higher level validation performed.
Change your custom validator to be:
def token_match
if token != User.find(user_id).verification_token
errors.add(:sms_code, "does not match")
end
end
and add in your model after_validation callback to be like this:
after_validation: increase_fails_count
def increase_fails_count
unless self.errors[:sms_code].empty?
user = User.find_by(:id => user_id)
user.increment!(:verification_fails)
user.save
end
end
You can use #update_columns in your validator. It writes directly to db.
u = User.find(user_id)
u.update_columns(verification_fails: u.verification_fails + 1)
This worked for me. But if for some reason it doesn't work for you, maybe you can try running it in a new thread,which creates a new db connection:
Thread.new do
num = User.find(user_id).verification_fails
ActiveRecord::Base.connection_pool.with_connection { |con| con.exec_query("UPDATE users SET verification_fails = #{num} WHERE id = #{user_id}") }
end.join

Rollback in Rails?

In my Rails application, I have a method which copies many rows, and also goes on to copy down some of the parent-child relationships.
def merge
params[:merge_rows].each do |merge_row|
batch_detail = BatchDetail.find(merge_row)
batch_detail.duplicate
batch_detail.batch_id = batch.id
batch_detail.save
end
render nothing: true
end
# BatchDetail.duplicate
def duplicate
batch_detail = dup
batch_detail.primer3_parameter = primer3_parameter.dup if primer3_parameter.present?
primer3_outputs.each do |primer3_output|
batch_detail.primer3_outputs << primer3_output.duplicate
end
batch_detail
end
Ideally, I would like to only save if all rows are successfully duplicated, and rollback all changes if any are unsuccessful.
Then I would like to report 200 or 500 via the render if successful or error.
wrap your ActiveRecord changes in a transaction block, if the end of the block is bypassed by some exception, all changes are rolled back.
begin
ActiveRecord::Base.transaction do
...various transactions
if (some_error_condition)
raise
end
end
...stuff to do if all successful
rescue
...stuff to do on failure
end

Rails - 'can't dump hash with default proc' during custom validation

I have 2 models. User and Want. A User has_many: Wants.
The Want model has a single property besides user_id, that's name.
I have written a custom validation in the Want model so that a user cannot submit to create 2 wants with the same name:
validate :existing_want
private
def existing_want
return unless errors.blank?
errors.add(:existing_want, "you already want that") if user.already_wants? name
end
The already_wants? method is in the User model:
def already_wants? want_name
does_want_already = false
self.wants.each { |w| does_want_already = true if w.name == want_name }
does_want_already
end
The validation specs pass in my model tests, but my feature tests fail when i try and submit a duplicate to the create action in the WantsController:
def create
#want = current_user.wants.build(params[:want])
if #want.save
flash[:success] = "success!"
redirect_to user_account_path current_user.username
else
flash[:validation] = #want.errors
redirect_to user_account_path current_user.username
end
end
The error I get: can't dump hash with default proc
No stack trace that leads to my code.
I have narrowed the issue down to this line:
self.wants.each { |w| does_want_already = true if w.name == want_name }
if I just return true regardless the error shows in my view as I would like.
I don't understand? What's wrong? and why is it so cryptic?
Thanks.
Without a stack trace (does it lead anywhere, or does it just not appear?) it is difficult to know what exactly is happening, but here's how you can reproduce this error in a clean environment:
# initialize a new hash using a block, so it has a default proc
h = Hash.new {|h,k| h[k] = k }
# attempt to serialize it:
Marshal.dump(h)
#=> TypeError: can't dump hash with default proc
Ruby can't serialize procs, so it wouldn't be able to properly reconstitute that serialized hash, hence the error.
If you're reasonably sure that line is the source of your trouble, try refactoring it to see if that solves the problem.
def already_wants? want_name
wants.any? {|want| want_name == want.name }
end
or
def already_wants? want_name
wants.where(name: want_name).count > 0
end

Rails: How to check if "update_attributes" is going to fail?

To check if buyer.save is going to fail I use buyer.valid?:
def create
#buyer = Buyer.new(params[:buyer])
if #buyer.valid?
my_update_database_method
#buyer.save
else
...
end
end
How could I check if update_attributes is going to fail ?
def update
#buyer = Buyer.find(params[:id])
if <what should be here?>
my_update_database_method
#buyer.update_attributes(params[:buyer])
else
...
end
end
it returns false if it was not done, same with save. save! will throw exceptions if you like that better. I'm not sure if there is update_attributes!, but it would be logical.
just do
if #foo.update_attributes(params)
# life is good
else
# something is wrong
end
http://apidock.com/rails/ActiveRecord/Base/update_attributes
Edit
Then you want this method you have to write. If you want to pre check params sanitation.
def params_are_sanitary?
# return true if and only if all our checks are met
# else return false
end
Edit 2
Alternatively, depending on your constraints
if Foo.new(params).valid? # Only works on Creates, not Updates
#foo.update_attributes(params)
else
# it won't be valid.
end
The method update_attributes returns false if object is invalid. So just use this construction
def update
if #buyer.update_attributes(param[:buyer])
my_update_database_method
else
...
end
end
If your my_update_database_method has to be call only before update_attributes, then you shoud use merge way, probably like this:
def update
#buyer = Buyer.find(params[:id])
#buyer.merge(params[:buyer])
if #buyer.valid?
my_update_database_method
#buyer.save
else
...
end
end
This may not be the best answer, but it seems to answer your question.
def self.validate_before_update(buyer)#parameters AKA Buyer.validate_before_update(params[:buyer])
# creates temporary buyer which will be filled with parameters
# the temporary buyer is then check to see if valid, if valid returns fail.
temp_buyer = Buyer.new
# populate temporary buyer object with data from parameters
temp_buyer.name = buyer["name"]
# fill other required parameters with valid data
temp_buyer.description = "filler desc"
temp_buyer.id = 999999
# if the temp_buyer is not valid with the provided parameters, validation fails
if temp_buyer.valid? == false
temp_buyer.errors.full_messages.each do |msg|
logger.info msg
end
# Return false or temp_buyer.errors depending on your need.
return false
end
return true
end
you'd better check it in your model through a before_save
before_save :ensure_is_valid
private
def ensure_is_valid
if self.valid?
else
end
end
I've run into the same scenario - needed to know if record is valid and do some actions before update save. I've found out that there is assign_attributes(attributes) method which update method uses before save. So nowadays it's likely correct to do:
def update
#buyer = Buyer.find(params[:id])
#buyer.assign_attributes(params[:buyer])
if #buyer.valid?
my_update_database_method
#buyer.save
else
...
end
end

In ActiveRecord how do I use 'changed' (dirty) in a before_save callback?

I want to set my summary field to a sanitized version of the body field, but only if the user does not supply their own summary ie. params[:document][:summary] is blank.
This appears to work fine if I create a new record, if I enter a summary it is saved, if I don't the body is used to generate the summary.
However when I update the record the summary always gets overridden. From my log files I can see that 'generate_summary' gets called twice, the second time the 'changes' hash is empty.
class Document << ActiveRecord::Base
# Callbacks
before_save :generate_summary
private
def generate_summary
#counter ||= 1
logger.debug '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$'
logger.debug #counter.to_s
logger.debug 'changes: ' + self.changes.inspect
self.summary = Sanitize.clean(self.body).to(255) if self.body && (!self.summary_changed? or self.summary.blank?)
#counter = #counter + 1
end
Log on Update:
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
1
changes: {"summary"=>["asdasdasdasd", "three.co.uk"]}
Page Update (0.7ms) UPDATE documents SET meta_description = 'three.co.uk', summary = 'three.co.uk', updated_at = '2009-09-30 11:37:08' WHERE id = 77
SQL (0.6ms) COMMIT
SQL (0.1ms) BEGIN
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
2
changes: {}
Page Update (0.5ms) UPDATE documents SET meta_description = 'asdasdasdasd', summary = 'asdasdasdasd', updated_at = '2009-09-30 11:37:08' WHERE id = 77
Your controller probably saves twice as said by #nasmorn. You can also check that your body as changed before updating your summary.
if self.body_changed? && (!self.summary_changed? or self.summary.blank?)
self.summary = Sanitize.clean(self.body).to(255)
end
Only logical explanation is that the controller somehow saves twice.
Is this log from the console where you call update on the record or is it from a real request that comes through the controller?
It seems 'update_attributes' triggers the before_save callback, so in my controller 'generate_summary' is called twice once by 'update_attributes' and once by 'save'. This is not expected behaviour.
Checking the body has changed as suggested by #vincent seems to prevent the unexpected behaviour.
The way I bypass the multiple before_save calls is introducing
attr_accessor :object_saved
and them inside the call back method
before_save :before_save_method
I do this
def before_save_method
if self.object_saved.nil?
self.object_saved = true
# Do Something
end
end

Resources