Use before_destroy to delete another model's entry? - ruby-on-rails

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.

Related

Rails Has-Many Relationship: Create not saving records into the database?

If I were to assume the models:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
When I'm trying to run User.first.posts.create [attributes], the model gets created, but its id is nil and is not saved in the database. Could anyone explain why? I thought this sort of behaviour was expected from #new, not from #create.
Your expectations are wrong.
Creates an object (or multiple objects) and saves it to the database, if validations pass. The resulting object is returned whether the object was saved successfully to the database or not.
The implementaton is actually dead simple:
def create(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
object = new(attributes, &block)
object.save
object
end
end
So for example when you do:
post = Post.create(title: 'Not a valid post')
This will always return an instance of post. If you then inspect the object you can see that its not persisted and the errors object will tell you why:
post.valid? # false
post.persisted? # false
post.errors.full_messages # ["User can't be blank."]
If you create the record off an association you're also mutating it:
user = User.first
post = user.posts.create(attributes)
user.posts.to_a.include?(post) # true
This might seem like undesirable behavior but is needed for nested attributes to work properly.
Use create! (which raises an error if the record is invalid) instead in non-interactive contexts (like seed files) where creating a record should not be expected to fail. Don't use it your controllers where is invalid input is not an exceptional event - and where its just a crutch akin to throwing a bunch of log or puts statements all over the code. Write tests that cover invalid and valid input instead.
If you really need to use a debugger like byebug or pry to step into the controller action and inspect the model instance.

ActiveRecord is not reloading nested object after it's updated inside a transaction

I'm using Rails 4 with Oracle 12c and I need to update the status of an User, and then use the new status in a validation for another model I also need to update:
class User
has_many :posts
def custom_update!(new_status)
relevant_posts = user.posts.active_or_something
ActiveRecord::Base.transaction do
update!(status: new_status)
relevant_posts.each { |post| post.update_stuff! }
end
end
end
class Post
belongs_to :user
validate :pesky_validation
def update_stuff!
# I can call this from other places, so I also need a transaction here
ActiveRecord::Base.transaction do
update!(some_stuff: 'Some Value')
end
end
def pesky_validation
if user.status == OLD_STATUS
errors.add(:base, 'Nope')
end
end
end
However, this is failing and I receive the validation error from pesky_validation, because the user inside Post doesn't have the updated status.
The problem is, when I first update the user, the already instantiated users inside the relevant_posts variable are not yet updated, and normally all I'd need to fix this was to call reload, however, maybe because I'm inside a transaction, this is not working, and pesky_validation is failing.
relevant_users.first.user.reload, for example, reloads the user to the same old status it had before the update, and I'm assuming it's because the transaction is not yet committed. How can I solve this and update all references to the new status?

Rails: How to prevent the deletion of records?

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...

How Rails: If a project has tasks it should not be deleted: how can I fix this?

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

How to save something to the database after failed ActiveRecord validations?

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.

Resources