I have a model Country and therefore a table countries. The countries table act as a collection of iso country and currency codes and should never reduce there content (after I have filled it with seed data). Because Country is a subclass of ActiveRecord::Base it inherits class methods like destroy, delete_all and so forth which deletes records. I'm looking for a solution to prevent the deletion of records at the model level.
Ofc. I know that I can make use of the object oriented approach to solve this problem by overriding this methods (and raise for instance an error when they called), but this assumes that I have to know all the inherited methods of the base class. I would be glad if someone could offer a more elegant solution.
Taking inspiration from Mark Swardstrom answer, I propose the following that is working also on Rails > 5.0:
Within your model:
before_destroy :stop_destroy
def stop_destroy
errors.add(:base, :undestroyable)
throw :abort
end
The following will make all calls to model.destroy return false and your model will not be deleted.
You can argue that still, calls to model.delete will work, and delete your record, but since these are lower level calls this makes perfectly sense to me.
You could also delete the records directly from the database if you want, but the above solution prevents deletion from the application level, which is the right place to check that.
Rubocop checks your calls to delete or delete_all and raises a warning, so you can be 100% sure that if you call model.delete is because you really want it.
My solution works on latest Rails versions where you need to throw :abort instead of returning false.
There's a before_destroy callback, maybe you could take advantage of that.
before_destroy :stop_destroy
def stop_destroy
self.errors[:base] << "Countries cannot be deleted"
return false
end
Rails 4+ (including Rails 6)
In our case, we wanted to prevent an object from being destroyed if it had any associated records. This would be unexpected behaviour, so we wanted an exception raised with a helpful error message.
We used the native ActiveRecord::RecordNotDestroyed class with a custom message as described below.
class MyClass < ApplicationRecord
has_many :associated_records
before_destroy :check_if_associated_records
private
def check_if_associated_records
# set a guard clause to check whether the record is safe to destroy
return unless associated_records.exists?
raise ActiveRecord::RecordNotDestroyed, 'Cannot destroy because...'
end
end
Behaviour with destroy and destroy!
my_class = MyClass.first.destroy
# => false
my_class = MyClass.first.destroy!
# => ActiveRecord::RecordNotDestroyed (Cannot destroy because...)
Note: If you have a belongs_to or has_one instead of a has_many association, your method will look like:
def check_if_associated_records
# set a guard clause to check whether the record is safe to destroy
return unless associated_record
raise ActiveRecord::RecordNotDestroyed, 'Cannot destroy because...'
end
In Rails 6 this is how I prevent records from being deleted.
The exception raised will roll back the transaction that is being used by ActiveRecord therefore preventing the record being deleted
class MenuItem < ApplicationRecord
after_destroy :ensure_home_page_remains
class Error < StandardError
end
protected #or private whatever you need
#Raise an error that you trap in your controller to prevent your record being deleted.
def ensure_home_page_remains
if menu_text == "Home"
raise Error.new "Can't delete home page"
end
end
So the ensure_home_page_remains method raises an MenItem::Error that causes the transaction to be rolled back which you can trap in your controller and take whatever appropriate action you feel is necessary, normally just show the error message to the user after redirecting to somewhere. e.g.
# DELETE /menu_items/1
# DELETE /menu_items/1.json
def destroy
#menu_item.destroy
respond_to do |format|
format.html { redirect_to admin_menu_items_url, notice: 'Menu item was successfully destroyed.' }
format.json { head :no_content }
end
end
#Note, the rescue block is outside the destroy method
rescue_from 'MenuItem::Error' do |exception|
redirect_to menu_items_url, notice: exception.message
end
private
#etc...
Related
I'm trying to prevent deletion of models from the db and pretty much follow this guide (see 9.2.5.3 Exercise Your Paranoia with before_destroy) from a Rails 4 book.
I have a simple model:
class User < ActiveRecord::Base
before_destroy do
update_attribute(:deleted_at, Time.current)
false
end
and in the controller:
def destroy
#user = User.find(params[:id])
# #user.update!(deleted_at: Time.zone.now) # if I do it here it works
#user.destroy # if I also comment this line...
render :show
end
The callback gets called and the attribute gets set, but then the database transaction always gets rolled back. It I leave out the returning of false the model gets deleted because the execution of delete is not halted.
As you can see in the comments I can get it to work but what I really want to do is use a Service Object and put the logic out of the controller.
if your callback returns false the transaction will always be rollbacked.
For what you want you should not call to the destroy method on your arel object.
Instead, make your own method like soft_destroy or something like that and update your attribute.
And to prevent others from calling the destroy method on your arel object, just add a callback raising and exception for instance.
Your model is just an object. If you really want to change the concept of destroy, change it:
def destroy
condition ? alt_action : super
end
I'm using the gem Responders but I'm not able to show errors that I create on my models using erros.add(:base, 'Error message').
On my controller, before the respond_with #app, I debugged the #app object and it has errors #app.errors.any? returns true
On my view, when I check the flash and #app objects, none has the error
App controller
# app_controllers.rb
def destroy
#app = current_company.apps.find(params[:id])
#app.destroy
respond_with #app
end
App model
# app.rb
before_destroy :destroy_on_riak
# ...
def destroy_on_riak
# SOME CODE HERE
rescue Exception => e
errors.add(:base, I18n.t("models.app.could_not_destroy", :message => e.message))
return false
end
App view
# apps.html.haml
-flash.each do |name, msg|
%div{:class => "flash #{name}"}
=content_tag :p, msg if msg.is_a?(String)
This is the #app object before the #app.destroy
"#<ActiveModel::Errors:0x00000004fa0510 #base=#<App id: 34, ...>, #messages={}>"
This is the #app object after the #app.destroy
"#<ActiveModel::Errors:0x00000004fa0510 #base=#<App id: 34, ...>, #messages={:base=>[\"Não foi possível excluir a aplicação: undefined method `get_or_new' for #<App:0x00000004f824c0>\"]}>"
I have removed what's inside the #base= for simplicity.
jefflunt is correct, when one calls #valid? it clears the errors array: see https://github.com/rails/rails/blob/4a19b3dea650351aa20d0cad64bf2d5608023a33/activemodel/lib/active_model/validations.rb#L333
The validators are designed to 100% determine the validity of your object, not when you add errors yourself for later use.
While ActiveRecord does override #valid?, it still calls super: https://github.com/rails/rails/blob/4a19b3dea650351aa20d0cad64bf2d5608023a33/activerecord/lib/active_record/validations.rb#L58
If you want to add errors and have them persist I recommend something like this:
def failures
#failures ||= []
end
validate :copy_failures_to_errors
def copy_failures_to_errors
#failures.each { |f| errors.add(*f) }
end
Then modify your rescue:
def destroy_on_riak
# SOME CODE HERE
rescue Exception => e
failures.push([:base, I18n.t("models.app.could_not_destroy", :message => e.message)])
return false
end
I know this seems convoluted and I know there are even examples online where people use or recommend errors.add(:base, ...), but it is not a safe way to store and retrieve errors for later.
Also, just a recommendation, but I advise you to rescue StandardError and not Exception unless you absolutely must. Out of memory errors and stuff like that are Exceptions, but every normal error you would ever want to rescue should inherit from StandardError. Just FYI.
The mystery seems to be either (a) you might not be calling the right method, or (b) the .errors hash doesn't contain what you think it contains.
Wrong method?
In your controller you're calling #app.destroy, but the method that adds the errors is called destroy_on_riak
Are you sure you don't mean to type this?
# app_controllers.rb
def destroy
#app = current_company.apps.find(params[:id])
#app.destroy_on_riak # <= The offending line?
respond_with #app
end
Or is there a before_destroy callback missing from your code sample that in turn calls destroy_on_riak? From the code included I don't see anywhere that the destroy_on_riak method ever gets called, so this is just a guess.
Unexpected contents of .errors hash?
If that's not the problem, then when #app.errors.any? is returning true, then at that point in the code print the contents of #app.errors to your log so you can see what's wrong.
I will give you some hints:
Hint 1
Your form may be calling valid? on the #app object. The valid? method clears the errors array on the instance.
It is not correct to use the errors array/construct outside of the validations context. But this is MHO.
Hint 2
According to Responders gem (which I have never used in the past), your locale just needs to have the correct configuration. Example:
flash:
actions:
create:
notice: "{resource_name} was successfully created"
update:
notice: "{resource_name} was successfully updated"
destroy:
notice: "{resource_name} was successfully destroyed"
alert: "{resource_name} could not be destroyed"
Does it?
I'd have to agree with #p.mastinopoulos on this. This should really be handled with the builtin validations. Sounds like you are in need of building a custom validator.
http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate
Try replacing your before_destroy with a validate:
validate :destroy_on_riak, on: :destroy
Haven't tried this, but if it doesn't work, you may want to consider creating a custom Validator as referenced in the docs.
Hi I have a project and each project has tasks. A task belongs to a project. Before I delete a project I want to check if there are related tasks. If there are tasks I don't want to delete the project. If there are no associated tasks, the project should be deleted. Can you please help me with the code? What am I missing?
class Project < ActiveRecord::Base
before_destroy :check_tasks
def check_tasks
if Project.find(params[:id]).tasks
flash[:notice] = 'This project has tasks.'
redirect_to :action => 'list_projects'
end
end
end
Return false from the before_destroy method to prevent the instance from being destroyed.
The method should also return a meaningful error for troubleshooting.
class Project < ActiveRecord::Base
before_destroy :check_tasks
def check_tasks
if self.tasks.any?
errors.add_to_base "Project has tasks and cannot be destroyed."
return false
end
end
end
Note: flash[:notice] and params[:attr_name] can only be used from controllers.
You have a couple of problems here.
You don't (or shouldn't) have access to the params variable (it's available in controllers and views only, unless you're passing it to the model, which is probably not what you want).
Your if checks against project.tasks which is an array - even an empty array evaluates to true, so your other code branch will never occur no matter if the project has tasks or not.
You should probably be setting error messages for the view from your ProjectsController#destroy action, not in your model.
Solutions:
Change Project.find(params[:id]) to self - you want to check the tasks for every instance of the Project.
Change the check in your if statement from if self.tasks to if self.tasks.any? which returns the value you want (false if the array is empty, true otherwise).
Move the flash[:notice] from your model to your controller, as well as the redirect_to call, where they belong. This means your check_tasks method can be changed to the following:
code:
def check_tasks
return !self.tasks.any?
end
Should the check be self instead? (not sure where you getting the params[:id] from).
Haven't checked this out yet though - but since I need something similar for my Users model I'll see how that works out and get back to you.
class Project < ActiveRecord::Base
before_destroy :check_tasks
private
def check_tasks
#edited
if tasks.empty?
false
end
end
So I have a model, let's call it Notes. On the notes, you can but several posts. So the notes model has the line:
has_many :posts
And the posts model has the line
belongs_to :note
Now, when a User destroys a post, I want for the note to be destroyed IF it not longer has any other posts.
I thought i would write this code into the post model with before_destroy:
def delete_note_if_last_post
if self.note.posts.count == 1
self.note.destroy
end
end
This doesn't work. It shuts down the server based on an "Illegal Instruction". Is there some way to accomplish what I am trying to do?
EDIT: changed the code, as I noticed an error, and now the problem is slightly different.
you can return false to prevent a model from destruction in before_destroy filter
before_destroy :has_no_post
then in has_no_post
def has_no_post
#You can prevent this from deletion by using these options
#Option1 return false on certain condition
return false if posts.any?
#or add an error to errors
errors << "Can not delete note if it has post" if posts.any?
#raise an exception
raise "Cant delete ..." if blah blah
end
I would suggest putting this kind of logic into an observer. Something like
class PostObserver < ActiveRecord::Observer
def after_destroy(post)
note = Note.find(post.note_id)
note.destroy if note.posts.count == 0
end
end
You'd have to register the observer in your config/application.rb file as well. One thing to note is that if your callback returns any value that can be evaluated as false (e.g. nil or false) the rest of your callbacks will not run.
Basically what I want to do is to log an action on MyModel in the table of MyModelLog. Here's some pseudo code:
class MyModel < ActiveRecord::Base
validate :something
def something
# test
errors.add(:data, "bug!!")
end
end
I also have a model looking like this:
class MyModelLog < ActiveRecord::Base
def self.log_something
self.create(:log => "something happened")
end
end
In order to log I tried to :
Add MyModelLog.log_something in the something method of MyModel
Call MyModelLog.log_something on the after_validation callback of MyModel
In both cases the creation is rolled back when the validation fails because it's in the validation transaction. Of course I also want to log when validations fail. I don't really want to log in a file or somewhere else than the database because I need the relationships of log entries with other models and ability to do requests.
What are my options?
Nested transactions do seem to work in MySQL.
Here is what I tried on a freshly generated rails (with MySQL) project:
./script/generate model Event title:string --skip-timestamps --skip-fixture
./script/generate model EventLog error_message:text --skip-fixture
class Event < ActiveRecord::Base
validates_presence_of :title
after_validation_on_create :log_errors
def log_errors
EventLog.log_error(self) if errors.on(:title).present?
end
end
class EventLog < ActiveRecord::Base
def self.log_error(event)
connection.execute('BEGIN') # If I do transaction do then it doesn't work.
create :error_message => event.errors.on(:title)
connection.execute('COMMIT')
end
end
# And then in script/console:
>> Event.new.save
=> false
>> EventLog.all
=> [#<EventLog id: 1, error_message: "can't be blank", created_at: "2010-10-22 13:17:41", updated_at: "2010-10-22 13:17:41">]
>> Event.all
=> []
Maybe I have over simplified it, or missing some point.
Would this be a good fit for an Observer? I'm not sure, but I'm hoping that exists outside of the transaction... I have a similar need where I might want to delete a record on update...
I've solved a problem like this by taking advantage of Ruby's variable scoping. Basically I declared an error variable outside of a transaction block then catch, store log message, and raise the error again.
It looks something like this:
def something
error = nil
ActiveRecord::Base.transaction do
begin
# place codez here
rescue ActiveRecord::Rollback => e
error = e.message
raise ActiveRecord::Rollback
end
end
MyModelLog.log_something(error) unless error.nil?
end
By declaring the error variable outside of the transaction scope the contents of the variable persist even after the transaction has exited.
I am not sure if it applies to you, but i assume you are trying to save/create a model from your controller. In the controller it is easy to check the outcome of that action, and you most likely already do to provide the user with a useful flash; so you could easily log an appropriate message there.
I am also assuming you do not use any explicit transactions, so if you handle it in the controller, it is outside of the transaction (every save and destroy work in their own transaction).
What do you think?
MyModelLog.log_something should be done using a different connection.
You can make MyModelLog model always use a different connection by using establish_connection.
class MyModelLog < ActiveRecord::Base
establish_connection Rails.env # Use different connection
def self.log_something
self.create(:log => "something happened")
end
end
Not sure if this is the right way to do logging!!
You could use a nested transaction. This way the code in your callback executes in a different transaction than the failing validation. The Rails documentations for ActiveRecord::Transactions::ClassMethods discusses how this is done.