Object can not be deleted is attribute is active - Rspec RoR - ruby-on-rails

I'm trying to test if object is active and it is active then it can not be deleted. So for i have this in my plan_spec.rb:
it "can not be deleted if active" do
plan = Plan.new(active: true)
r = plan.destroy
r.should_not be_valid
end
Name of the attribute i'm trying to check is 'active' and it is boolean, so if active is true then it can't object plan can not be deleted.
Any help?

It can be achieved by using before_destroy callback which will return false if record can't be destroyed:
class Plan < ActiveRecord::Base
# ...
before_destroy :check_if_active
# ...
private
def check_if_active
!active?
end
end
With this solution, you should also rewrite your test, cause you shouldn't check validity:
it 'can not be deleted if active' do
plan = Plan.create!(active: true)
expect { plan.destroy }.to_not change { Plan.count }
end

in your Plan class, add:
before_destroy :ensure_inactive
#will abort destroy if return false
def ensure_inactive
!active?
end
Btw, your spec is wrong, its not really a validation. You should:
save the object
call destroy
ensure it has not been destroyed

Related

Custom Ruby/rails model validation not working

I have created a custom validation for a ruby model (using the luhn algorithm). Even when I explicitly just return false from the custom validation, the object still saves.
This is what I have in my CreditCard model:
before_save :check_card_number
private
def check_card_number
return false unless card_number_luhn
end
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
but even if I just return false:
before_save :check_card_number
private
def check_card_number
return false
end
#so this is never even called
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
the object still saves. This is true EVEN IF I use validate instead of before_save. What is going on?
From Rails 5, you need to explicitly call throw(:abort)
# ...
before_save :check_card_number
# ...
def card_number_luhn
valid = #luhn_algorithm_here_that_returns_true_or_false
throw(:abort) unless valid
end
another way:
ActiveSupport.halt_callback_chains_on_return_false = true
reference: https://www.bigbinary.com/blog/rails-5-does-not-halt-callback-chain-when-false-is-returned

after_destroy callback claims object still exists

I have an after_destroy callback that I'd expect to return nil but instead still has a value.
class WeighIn < ActiveRecord::Base
belongs_to :check_in
after_destroy :add_employee_weightloss
def add_employee_weightloss
p self.check_in.weigh_in.present? # returns true
end
end
specs:
it "employee weightloss" do
ci = CheckIn.create()
wi = WeighIn.create(check_in_id: ci.id)
wi.destroy
expect(wi.reload).to eq(nil) # returns wi instead of nil
end
You should use destroyed? (or exists?, or persisted?) instead, cause present? just checks if the object is present, which is the correct behaviour after destruction (destroy itself returns deleted object).
def add_employee_weightloss
p check_in.weigh_in.destroyed?
end
Also you should not use the following:
expect(wi.reload).to eq(nil)
cause if wi was destroyed you are going to get ActiveRecord::RecordNotFound exception instead of nil. You can try the following:
it "employee weightloss" do
wi = WeighIn.create(check_in: CheckIn.create)
wi.destroy
expect(wi.destroyed?).to eq(true)
end

Manually fail #update_attributes save in Rails

I'm calling #foo.update and inside 1 of the attributes it's updating, I'm calling a the write method (def attribute=) in foo's model class and want it to fail the entire update conditionally. What can I put in there? I tried using errors[:base] but it doesn't fail the save. I can't use validates either because the attribute will be transformed into something else before it gets saved.
def attribute=(attr)
if bar
# code to fail entire db save
end
end
You can just check the condition on a before_save callback in model foo.rb and return false if you don't want to save it.
before_save :really_want_to_save?
private
def really_want_to_save?
conditional_says_yes ? true : false
end
If you want error message too, then
def really_want_to_save?
if conditional_says_yes
true
else
errors[:base] << "failed"
false
end
end
If you want to abort from within the setter, then raising an exception will suffice.
def attribute=(attr)
if bar
raise "Couldn't save because blah blah"
end
end
However, as mentioned in other posts, it is likely better to do this check before save. That's what validation is for.
validate :my_condition
def my_condition
if bar
errors.add(:base, "Couldn't save because blah blah")
end
end

Active Record Clear instance of model after_find callback

I have defined a callback after_find for checking some settings based on the retrieved instance of the model. If the settings aren't fulfilled I don't want the instance to be return from the find method. Is that possible?
an example
the controller looks like:
class UtilsController < ApplicationController
def show
#util = Util.find(params[:id])
end
end
the model:
class Util < ActiveRecord::Base
after_find :valid_util_setting
def valid_util_setting
# calculate_availability? complex calculation
# that can not be part of the sql statement or a scope
unless self.setting.calculate_availability?(User.current.session)
#if not available => clear the record for view
else
#nothing to do here
end
end
end
Instead of trying to clear the record, you could just raise an exception?
E.g.
unless self.setting.calculate_availability?(User.current.session)
raise ActiveRecord::RecordNotFound
else
...
I'm afraid you can't clear found record in this callback
Maybe you should find in scope with all your options from the beginning?
I.e. #util = Util.scoped.find(params[:id])
I found a solution
def valid_util_setting
Object.const_get(self.class.name).new().attributes.symbolize_keys!.each do |k,v|
begin
self.assign_attributes({k => v})#, :without_protection => true)
rescue ActiveModel::MassAssignmentSecurity::Error => e; end
end
end
With this I'm able to create an almost empty object

How to simplify the soft delete process with Ruby on Rails?

I want to have a model where I need to soft delete a record and not show them in the find or any other conditions while searching.
I want to retain the model without deleting the record. How to go about this?
Just use a concern in rails 4
Example here
module SoftDeletable
extend ActiveSupport::Concern
included do
default_scope { where(is_deleted: false) }
scope :only_deleted, -> { unscope(where: :is_deleted).where(is_deleted: true) }
end
def delete
update_column :is_deleted, true if has_attribute? :is_deleted
end
def destroy;
callbacks_result = transaction do
run_callbacks(:destroy) do
delete
end
end
callbacks_result ? self : false
end
def self.included(klazz)
klazz.extend Callbacks
end
module Callbacks
def self.extended(klazz)
klazz.define_callbacks :restore
klazz.define_singleton_method("before_restore") do |*args, &block|
set_callback(:restore, :before, *args, &block)
end
klazz.define_singleton_method("around_restore") do |*args, &block|
set_callback(:restore, :around, *args, &block)
end
klazz.define_singleton_method("after_restore") do |*args, &block|
set_callback(:restore, :after, *args, &block)
end
end
end
def restore!(opts = {})
self.class.transaction do
run_callbacks(:restore) do
update_column :is_deleted, false
restore_associated_records if opts[:recursive]
end
end
self
end
alias :restore :restore!
def restore_associated_records
destroyed_associations = self.class.reflect_on_all_associations.select do |association|
association.options[:dependent] == :destroy
end
destroyed_associations.each do |association|
association_data = send(association.name)
unless association_data.nil?
if association_data.is_deleted?
if association.collection?
association_data.only_deleted.each { |record| record.restore(recursive: true) }
else
association_data.restore(recursive: true)
end
end
end
if association_data.nil? && association.macro.to_s == 'has_one'
association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
end
end
clear_association_cache if destroyed_associations.present?
end
end
Deletable
A rails concern to add soft deletes.
Very simple and flexible way to customise/ change
(You can change the delete column to be a timestamp and change the methods to call ActiveRecord touch ).
Best where you want to control code not have gems for simple tasks.
Usage
In your Tables add a boolean column is_deletable
class AddDeletedAtToUsers < ActiveRecord::Migration
def change
add_column :users, :is_deleted, :boolean
end
end
In your models
class User < ActiveRecord::Base
has_many :user_details, dependent: :destroy
include SoftDeletable
end
Methods and callbacks available:
User.only_deleted
User.first.destroy
User.first.restore
User.first.restore(recursive: true)
Note:
Focus Using update_column or touch if you decide to use a timestamp column.
Edited
If you are using rails <= 3.x (this example also use a DateTime field instead boolean), there are some differences:
module SoftDeletable
extend ActiveSupport::Concern
included do
default_scope { where(deleted_at: nil }
# In Rails <= 3.x to use only_deleted, do something like 'data = Model.unscoped.only_deleted'
scope :only_deleted, -> { unscoped.where(table_name+'.deleted_at IS NOT NULL') }
end
def delete
update_column :deleted_at, DateTime.now if has_attribute? :deleted_at
end
# ... ... ...
# ... OTHERS IMPLEMENTATIONS ...
# ... ... ...
def restore!(opts = {})
self.class.transaction do
run_callbacks(:restore) do
# Remove default_scope. "UPDATE ... WHERE (deleted_at IS NULL)"
self.class.send(:unscoped) do
update_column :deleted_at, nil
restore_associated_records if opts[:recursive]
end
end
end
self
end
alias :restore :restore!
def restore_associated_records
destroyed_associations = self.class.reflect_on_all_associations.select do |association|
association.options[:dependent] == :destroy
end
destroyed_associations.each do |association|
association_data = send(association.name)
unless association_data.nil?
if association_data.deleted_at?
if association.collection?
association_data.only_deleted.each { |record| record.restore(recursive: true) }
else
association_data.restore(recursive: true)
end
end
end
if association_data.nil? && association.macro.to_s == 'has_one'
association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
end
end
clear_association_cache if destroyed_associations.present?
end
end
Usage
In your Tables add a DateTime column deleted_at
class AddDeletedAtToUsers < ActiveRecord::Migration
def change
add_column :users, :deleted_at, :datetime
end
end
Try this gem : https://github.com/technoweenie/acts_as_paranoid - ActiveRecord plugin allowing you to hide and restore records without actually deleting them
Just add a boolean field called deleted or something to that effect. When you soft delete the record just set that field to true.
When doing a find just add that as a condition (or make a scope for it).
The default_scope functionality in ActiveRecord 3 makes this easy, but personally, I favor the wide variety of standard solutions that can be dropped into the project. acts_as_archive in particular is the best fit for most of my projects, since it moves infrequently-accessed deleted records to a separate table, allowing the base table to stay small and in the database server's RAM.
Depending on your needs, you may also want to consider versioning instead of soft deletion.
Add a date field to your model - deleted_at.
Override the delete (or destroy) method on your model to set the deleted_at value. You can also create it as a new method. Something like soft_delete.
Add a restore/undelete method to your model to set the deleted_at value back to null.
Optional: create an alias method for the original delete (or destroy) method. Name it something like hard_delete.
You can define a module like this
module ActiveRecordScope
def self.included(base)
base.scope :not_deleted, -> { base.where(deleted: false) }
base.send(:default_scope) { base.not_deleted }
base.scope :only_deleted, -> { base.unscope(where: :deleted).where(deleted: true) }
def delete
update deleted: true
end
def recover
update deleted: false
end
end
end
Then in your class, you can write something like:
class User < ActiveRecord::Base
include ActiveRecordScope
end
So you have both soft delete and recover.
You call user.delete to soft delete an user. Then you can call user.recover to set the deleted back to false again, and recover it.
Have a look at rails3_acts_as_paranoid.
A simple plugin which hides records instead of deleting them, being
able to recover them.
...
This plugin was inspired by acts_as_paranoid and acts_as_active.
Usage:
class Paranoiac < ActiveRecord::Base
acts_as_paranoid
scope :pretty, where(:pretty => true)
end
Paranoiac.create(:pretty => true)
Paranoiac.pretty.count #=> 1
Paranoiac.only_deleted.count #=> 0
Paranoiac.pretty.only_deleted.count #=> 0
Paranoiac.first.destroy
Paranoiac.pretty.count #=> 0
Paranoiac.only_deleted.count #=> 1
Paranoiac.pretty.only_deleted.count #=> 1
If you use Rails4, Try this gem : https://github.com/alfa-jpn/kakurenbo
An association function of Kakurenbo is better than other gems.
i wont use a default scope cause if i want to get all the records, i need to ignore it using "with_exclusive_scope" which in turn is messy. the would go by adding a 'deleted' boolean field which is set when the record is deleted. Also, would have added scopes to get the data as per the condition.
checkout Overriding a Rails default_scope and Rails: Why is with_exclusive_scope protected? Any good practice on how to use it?
Add one column say status in your table and on deletion of records update the value of column status to inactive.
and while fetching the records, add condition status != "inactive" in the query.
For Rails 4 don't use acts_as_paranoid (buggy for Rails 4), use paranoia. All you have to do is add a deleted_at timestamp column and include acts_as_paranoia in the model.
From there, just call destroy on the object and all other ActiveRecord relations and most other methods(like :count) automatically exclude the soft_deleted records.

Resources