Unable to copy Ruby object's attributes - ruby-on-rails

I have a very painful issue here, if some one could help ?
I have a class Task with attributes and I wanted to make a bang method (because I'm changing the receiver) but I'm not able to change object attributes properly :
def reject!
return nil if self.previous_owner.nil?
self.delegate = false
# Before, owner equal a User object
self.owner = self.previous_owner # previous_owner is also a User object, but different
self.previous_owner = nil
return self if self.save
raise "Can't save object"
end
This won't work because self.owner will be nil...
I know this is because telling Ruby that self.owner = self.previous_owner and then changing self.previous_owner to nil.
So how can I avoid this ? I tried clone and dup method, but I'm not sure I want to have a new copied User object...
Strange thing too, if I try this in rails console :
a = 'Foo'
b = a
a = 'New value'
puts a # "New value"
puts b # "Foo"
So I think I'm missing something...

Please note that self.owner and self.previous_owner are convenience methods provided by Rails. The actual fields should be called something like owner_id and previous_owner_id and contain the actual id of the User you want to link to.
So a simpler, and failsafe approach is to write
self.owner_id = self.previous_owner_id
self.previous_owner = nil # this will unlink the previous owner
self.save!
and self.save! will raise if the save fails.

Related

Modifying ActiveRecord models before preventing deletion

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

Rails 5 ActiveRecord - check if any results before using methods

In my Rails 5 + Postgres app I make a query like this:
user = User.where("name = ?", name).first.email
So this gives me the email of the first user with the name.
But if no user with this names exists I get an error:
NoMethodError (undefined method `email' for nil:NilClass)
How can I check if I have any results before using the method?
I can think if various ways to do this using if-clauses:
user = User.where("name = ?", name).first
if user
user_email = user.email
end
But this does not seem to be the most elegant way and I am sure Rails has a better way.
You can use find_by, returns the object or nil if nothing is found.
user = User.find_by(name: name)
if user
...
end
That being said you could have still used the where clause if you're expecting more than one element.
users = User.where(name: name)
if users.any?
user = users.first
...
end
Then there is yet another way as of Ruby 2.3 where you can do
User.where(name: name).first&.name
The & can be used if you're not sure if the object is nil or not, in this instance the whole statement would return nil if no user is found.
I use try a lot to handle just this situation.
user = User.where("name = ?", name).first.try(:email)
It will return the email, or if the collection is empty (and first is nil) it will return nil without raising an error.
The catch is it'll also not fail if the record was found but no method or attribute exists, so you're less likely to catch a typo, but hopefully your tests would cover that.
user = User.where("name = ?", name).first.try(:emial)
This is not a problem if you use the Ruby 2.3 &. feature because it only works with nil object...
user = User.where("name = ?", name).first&.emial
# this will raise an error if the record is found but there's no emial attrib.
You can always use User.where("name = ?", name).first&.email, but I disagree that
user = User.where("name = ?", name).first
if user
user_email = user.email
end
is particularly inelegant. You can clean it up with something like
def my_method
if user
# do something with user.email
end
end
private
def user
#user ||= User.where("name = ?", name).first
# #user ||= User.find_by("name = ?", name) # can also be used here, and it preferred.
end
Unless you really think you're only going to use the user record once, you should prefer being explicit with whatever logic you're using.

Changing Spree from_address

I am trying to change Spree 3.0 from_email
I added this line to my spree initialiser, but it does not work:
Spree::Store.current.mail_from_address = “x#x.com"
Do you know of any reason why not?
I also put it directly in my mailer decorator:
Spree::OrderMailer.class_eval do
def confirm_email_to_store(order, resend = false)
Spree::Store.current.mail_from_address = "x#x.com"
#order = order.respond_to?(:id) ? order : Spree::Order.find(order)
subject = (resend ? "[#{Spree.t(:resend).upcase}] " : '')
subject += "#{'Will Call' if #order.will_call} #{'Just to See' if #order.just_to_see} ##{#order.number}"
mail(to: ENV['STORE_EMAIL'], from: from_address, subject: subject)
end
end
This also did not work
Check you might have created multiple stores via checking Spree::Store.all
Also, spree use current store as store which updated last so you have to check that also
You can simply change the from email address in the Admin Panel under Configuration -> General settings:
Got it to work this way:
Spree::Store.current.update(mail_from_address: ENV["STORE_EMAIL"])
Looking here http://www.davidverhasselt.com/set-attributes-in-activerecord/ you can see that:
user.name = "Rob"
This regular assignment is the most common and easiest to use. It is
the default write accessor generated by Rails. The name attribute will
be marked as dirty and the change will not be sent to the database
yet.
Of course, spree initializers claims to do save in the databse, but it did not:
If a preference is set here it will be stored within the cache & database upon initialization.
Finally calling Spree::Store.current will pull from the database, so any unsaved changes will be lost:
scope :by_url, lambda { |url| where("url like ?", "%#{url}%") }
def self.current(domain = nil)
current_store = domain ? Store.by_url(domain).first : nil
current_store || Store.default
end
Fixing this bug in Spree would be prefrable, this is sort of a workaround

Spree error when using decorator with the original code

Need a little help over here :-)
I'm trying to extend the Order class using a decorator, but I get an error back, even when I use the exactly same code from source. For example:
order_decorator.rb (the method is exactly like the source, I'm just using a decorator)
Spree::Order.class_eval do
def update_from_params(params, permitted_params, request_env = {})
success = false
#updating_params = params
run_callbacks :updating_from_params do
attributes = #updating_params[:order] ? #updating_params[:order].permit(permitted_params).delete_if { |k,v| v.nil? } : {}
# Set existing card after setting permitted parameters because
# rails would slice parameters containg ruby objects, apparently
existing_card_id = #updating_params[:order] ? #updating_params[:order][:existing_card] : nil
if existing_card_id.present?
credit_card = CreditCard.find existing_card_id
if credit_card.user_id != self.user_id || credit_card.user_id.blank?
raise Core::GatewayError.new Spree.t(:invalid_credit_card)
end
credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present?
attributes[:payments_attributes].first[:source] = credit_card
attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id
attributes[:payments_attributes].first.delete :source_attributes
end
if attributes[:payments_attributes]
attributes[:payments_attributes].first[:request_env] = request_env
end
success = self.update_attributes(attributes)
set_shipments_cost if self.shipments.any?
end
#updating_params = nil
success
end
end
When I run this code, spree never finds #updating_params[:order][:existing_card], even when I select an existing card. Because of that, I can never complete the transaction using a pre-existent card and bogus gateway(gives me empty blanks errors instead).
I tried to bind the method in order_decorator.rb using pry and noticed that the [:existing_card] is actuality at #updating_params' level and not at #updating_params[:order]'s level.
When I delete the decorator, the original code just works fine.
Could somebody explain to me what is wrong with my code?
Thanks,
The method you want to redefine is not really the method of the Order class. It is the method that are mixed by Checkout module within the Order class.
You can see it here: https://github.com/spree/spree/blob/master/core/app/models/spree/order/checkout.rb
Try to do what you want this way:
Create file app/models/spree/order/checkout.rb with code
Spree::Order::Checkout.class_eval do
def self.included(klass)
super
klass.class_eval do
def update_from_params(params, permitted_params, request_env = {})
...
...
...
end
end
end
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

Resources