I have the following classes:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
puts "Product Destroy!"
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
puts "Category Destroy!"
end
end
Here, I am trying to override the destroy method where I eventually want to do this:
update_attribute(:deleted_at, Time.now)
When I run the following statement in Rails console: ProductCategory.destroy_all I get the following output
Category Destroy!
Category Destroy!
Category Destroy!
Note: I have three categories and each category has more than one Products. I can confirm it by ProductCategory.find(1).products, which returns an array of products. I have heard the implementation is changed in Rails 5. Any points on how I can get this to work?
EDIT
What I eventually want is, to soft delete a category and all associated products in one go. Is this possible? Or will ave to iterate on every Product object in a before destroy callback? (Last option for me)
You should call super from your destroy method:
def destroy
super
puts "Category destroy"
end
But I definitely wouldn't suggest that you overide active model methods.
So this is how I did it in the end:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
# run all callback around the destory method
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
I am returning true from the destroy does make update_attribute a little dangerous but I am catching exceptions at the ApplicationController level as well, so works well for us.
Related
Given below are two models associated with one to many relationship and it works fine
class User < ActiveRecord::Base
has_many :events, dependent: :destroy
end
but when I associate them one to one
class User < ActiveRecord::Base
has_one :event, dependent: :destroy
end
It gives me the following error
undefined method `build' for nil:NilClass
Events Controller
class EventsController < ApplicationController
def new
#event = current_user.event.build
end
def create
#event = current_user.event.build(event_params)
if #event.save
redirect_to #event
else
render 'new'
end
end
private
def event_params
params.require(:event).permit(:date, :time, :venue)
end
end
The reason it is throwing the error is because there is no event for that user. While that is how to build through association for a has_many relationship, it doesn't work for has_one. See the documentation on has_one where they say that calling .build will not work.
Instead use #event = current_user.create_event
Adding a has_one relationship will give you the following methods:
association(force_reload = false)
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
For has_one associations, Rails provides a special set of methods for building the association (documentation)
build_event
create_event
Both of these take a hash of attributes just like the build method of a has_many association does.
In your case, change current_user.event.build to current_user.build_event and current_user.event.build(event_params) to current_user.build_event(event_params)
How can I redirect to the next lesson that does not have userLesson (problem is lessons belongs to a course through a chapter)
Models:
class Course
has_many :lessons, through: :chapters
end
class Lesson
belongs_to :chapter
has_one :lecture, through: :chapter
end
class User
has_many :user_lessons
end
class UserLesson
#fields: user_id, lesson_id, completed(boolean)
belongs_to :user
belongs_to :lesson
end
class Chapter
has_many :lessons
belongs_to :lecture
end
here user_lessons_controller:
class UserLessonsController < ApplicationController
before_filter :set_user_and_lesson
def create
#user_lesson = UserLession.create(user_id: #user.id, lession_id: #lesson.id, completed: true)
if #user_lesson.save
# redirect_to appropriate location
else
# take the appropriate action
end
end
end
I want to redirect_to the next lesson that has not the UserLesson when saved. I have no idea how to do it as it belongs_to a chapter. Please help! Could you please help me with the query to write...
Here is the answer for your question:
Inside your user_lessons_controller:
def create
#user_lesson = UserLession.create(user_id: #user.id, lession_id: #lesson.id, completed: true)
if #user_lesson.save
#You have to determine the next_lesson object you want to redirect to
#ex : next_lessons = current_user.user_lessons.where(completed: false)
#This will return an array of active record UserLesson objects.
#depending on which next_lesson you want, you can add more conditions in `where`.
#Say you want the first element of next_lessons array. Do
##next_lesson = next_lessons.first
#after this, do:
#redirect_to #next_lesson
else
#redirect to index?? if so, add an index method in the same controller
end
end
This code will only work if you define show method in your UserLessonsController and add a show.html in your views.
Also, in config/routes.rb, add this line : resources :user_lessons.
Using Rails 4, I have an Org model which has_many Memberships. I also have a polymorphic Activity model where both Org and Membershipare trackable.
Whenever the user performs a create, update or destroy action, I use a transaction to track! the action in the Activity feed like this:
membership.transaction do
membership.destroy!
user.track! membership, :destroy
end
To validate the destroy action on Membership, I use a before_destroy callback since I'm in a transaction:
class Membership < ActiveRecord::Base
class LastAdminDelete < StandardError; end
before_destroy :is_last_admin
...
def is_last_admin
if admin? && org.admins.count == 1
errors[:base] << 'is the only Admin for this organization'
raise Membership::LastAdminDelete
end
end
end
Which led to a Controller.destroy pattern like this:
def destroy
begin
# perform delete transaction with tracking
# report success for html, json formats
rescue Membership::LastAdminDelete
# report validation errors for html, json formats
end
end
It's also possible that Rails could raise ActiveRecord::RecordNotDestroyed, but I defer that to Rails default controller exception handling.
However...this broke cascading deletes so I changed dependant: :destroy to dependent: :delete_all on the memberships relation to subvert my before_destroy callback. Suddenly things seemed a little hackish.
class Org < ActiveRecord::Base
has_many :users, through: :memberships
has_many :invitations, dependent: :destroy
has_many :memberships, dependent: :delete_all # avoid callbacks
...
end
Did I choose the right pattern to enforce my delete validation? Or is there a better one?
In my Rails 4 app I have the following models:
class Invoice < ActiveRecord::Base
has_many :allocations
has_many :payments, :through => :allocations
end
class Allocation < ActiveRecord::Base
belongs_to :invoice
belongs_to :payment
end
class Payment < ActiveRecord::Base
has_many :allocations, :dependent => :destroy
has_many :invoices, :through => :allocations
after_save :update_invoices
after_destroy :update_invoices # won't work
private
def update_invoices
invoices.each do |invoice|
invoice.save
end
end
end
The problem is that I need to update an invoice when one of its payments gets destroyed.
The update_invoices callback above obviously can't ever get triggered because at the time it gets called the connection with the invoice has already been destroyed.
So how can this be done?
Right now, I am doing this in my PaymentsController:
def destroy
#payment.destroy
current_user.invoices.each do |invoice|
invoice.save
end
...
end
However, this is very expensive of course because it goes through each and every invoice that a user has.
What might be a better alternative to this?
Thanks for any feedback.
One solution would be to grab the invoices before destroying the payment instance. Its add a bit more logic to the Controller however, but this is where the intent of both actions ( destroy payment and update invoices ) originate. It also reduces the iteration to just those invoices affected by the destroyed payment.
def destroy
invoices = #payment.invoices
#payment.destroy
invoices.each do |invoice|
invoice.save
end
...
end
Presumably you are overriding the save method of the Invoice model ( or have a callback on that as well), though I would choose a more explicit method for this intent. For example, removed_payment could be a method to handle this specific scenario and update the appropriate attributes - outstanding_amount and payment_status, etc.
def destroy
invoices = #payment.invoices
#payment.destroy
invoices.map(&:removed_payment)
...
end
The problem is that the associated allocation is also destroyed when destroying the payment. If you move the invoice updating to the Allocation model instead it will work as intended.
class Allocation < ActiveRecord::Base
belongs_to :invoice
belongs_to :payment
after_destroy :update_invoice
def update_invoice
if destroyed?
invoice.save!
end
end
end
Here's a Rails 4.1 test project with tests for this:
https://github.com/infused/update_parent_after_destroy
On destruction of a restful resource, I want to guarantee a few things before I allow a destroy operation to continue? Basically, I want the ability to stop the destroy operation if I note that doing so would place the database in a invalid state? There are no validation callbacks on a destroy operation, so how does one "validate" whether a destroy operation should be accepted?
You can raise an exception which you then catch. Rails wraps deletes in a transaction, which helps matters.
For example:
class Booking < ActiveRecord::Base
has_many :booking_payments
....
def destroy
raise "Cannot delete booking with payments" unless booking_payments.count == 0
# ... ok, go ahead and destroy
super
end
end
Alternatively you can use the before_destroy callback. This callback is normally used to destroy dependent records, but you can throw an exception or add an error instead.
def before_destroy
return true if booking_payments.count == 0
errors.add :base, "Cannot delete booking with payments"
# or errors.add_to_base in Rails 2
false
# Rails 5
throw(:abort)
end
myBooking.destroy will now return false, and myBooking.errors will be populated on return.
just a note:
For rails 3
class Booking < ActiveRecord::Base
before_destroy :booking_with_payments?
private
def booking_with_payments?
errors.add(:base, "Cannot delete booking with payments") unless booking_payments.count == 0
errors.blank? #return false, to not destroy the element, otherwise, it will delete.
end
It is what I did with Rails 5:
before_destroy do
cannot_delete_with_qrcodes
throw(:abort) if errors.present?
end
def cannot_delete_with_qrcodes
errors.add(:base, 'Cannot delete shop with qrcodes') if qrcodes.any?
end
State of affairs as of Rails 6:
This works:
before_destroy :ensure_something, prepend: true do
throw(:abort) if errors.present?
end
private
def ensure_something
errors.add(:field, "This isn't a good idea..") if something_bad
end
validate :validate_test, on: :destroy doesn't work: https://github.com/rails/rails/issues/32376
Since Rails 5 throw(:abort) is required to cancel execution: https://makandracards.com/makandra/20301-cancelling-the-activerecord-callback-chain
prepend: true is required so that dependent: :destroy doesn't run before the validations are executed: https://github.com/rails/rails/issues/3458
You can fish this together from other answers and comments, but I found none of them to be complete.
As a sidenote, many used a has_many relation as an example where they want to make sure not to delete any records if it would create orphaned records. This can be solved much more easily:
has_many :entities, dependent: :restrict_with_error
The ActiveRecord associations has_many and has_one allows for a dependent option that will make sure related table rows are deleted on delete, but this is usually to keep your database clean rather than preventing it from being invalid.
You can wrap the destroy action in an "if" statement in the controller:
def destroy # in controller context
if (model.valid_destroy?)
model.destroy # if in model context, use `super`
end
end
Where valid_destroy? is a method on your model class that returns true if the conditions for destroying a record are met.
Having a method like this will also let you prevent the display of the delete option to the user - which will improve the user experience as the user won't be able to perform an illegal operation.
I ended up using code from here to create a can_destroy override on activerecord:
https://gist.github.com/andhapp/1761098
class ActiveRecord::Base
def can_destroy?
self.class.reflect_on_all_associations.all? do |assoc|
assoc.options[:dependent] != :restrict || (assoc.macro == :has_one && self.send(assoc.name).nil?) || (assoc.macro == :has_many && self.send(assoc.name).empty?)
end
end
end
This has the added benefit of making it trivial to hide/show a delete button on the ui
You can also use the before_destroy callback to raise an exception.
I have these classes or models
class Enterprise < AR::Base
has_many :products
before_destroy :enterprise_with_products?
private
def empresas_with_portafolios?
self.portafolios.empty?
end
end
class Product < AR::Base
belongs_to :enterprises
end
Now when you delete an enterprise this process validates if there are products associated with enterprises
Note: You have to write this in the top of the class in order to validate it first.
Use ActiveRecord context validation in Rails 5.
class ApplicationRecord < ActiveRecord::Base
before_destroy do
throw :abort if invalid?(:destroy)
end
end
class Ticket < ApplicationRecord
validate :validate_expires_on, on: :destroy
def validate_expires_on
errors.add :expires_on if expires_on > Time.now
end
end
I was hoping this would be supported so I opened a rails issue to get it added:
https://github.com/rails/rails/issues/32376