Using a rails custom validation to modify data - ruby-on-rails

I would like to add some logic to my model to ensure there is a consistent string representation of a serial number in the database of devices. Below is an example of the sort of thing I'm thinking of: call a custom validation and do formatting on the data in the object before saving it to the database.
class Device < ActiveRecord::Base
validates_presence_of :serialNumber
validate :validate_sn
def validate_sn
if !serialNumber.nil? && serialNumber !~ /-/
serialNumber = serialNumber.scan(/.{4}/).join('-')
end
end
end
I'm having two problems with this code. First, it will throw an error that says
NoMethodError: undefined method `scan' for nil:NilClass
on line 6, which is baffling to me. I can work around this by changing that line to:
sn = serialNumber; serialNumber = sn.scan(/.{4}/).join('-')
But if someone could explain why that works but the first doesn't I'd appreciate it. The real problem is that the data is saved without dashes (after that fix above is applied). Am I forgetting or misunderstanding something fundamental here? Does anyone have a better solution for this?
Thanks,

Try
def validate_sn
if !self.serialNumber.nil? && self.serialNumber !~ /-/
self.serialNumber = self.serialNumber.scan(/.{4}/).join('-')
end
end

The first way doesn't work because you are using serialNumber in two different contexts.
In this line
if !serialNumber.nil? && serialNumber !~ /-/
you're using the method serialNumber, since this method doesn't exist, ruby will delegate to method_missing which will get the value from the #attributes hash.
But, in this line:
serialNumber = serialNumber.scan(/.{4}/).join('-')
you're using the serialNumber variable that you've just declared. This variable is of course nil.
One way of achieving what you want is:
self.serialNumber = self.serialNumber.scan(/.{4}/).join('-')
But as another user already said, modifying the data in a validation is not a good practice. So, this code should be in a before_save filter.

Related

Is there a more idiomatic way to update an ActiveRecord attribute hash value?

Given a person ActiveRecord instance: person.phones #=> {home: '00123', office: '+1-45'}
Is there a more Ruby/Rails idiomatic way to do the following:
person_phones = person.phones
person_phones[:home] = person_phones[:home].sub('001', '+1')
person.update_column :phones, person_phones
The example data is irrelevant.
I only want to sub one specific hash key value and the new hash to be saved in the database. I was wondering if there was a way to do this just calling person.phones once, and not multiple times
Without changing much behaviour:
person.phones[:home].sub!('001', '+1')
person.save
There are a few important differences here:
You modify the string object by using sub! instead of sub. Meaning that all other variables/objects that hold a reference to the string will also change.
I'm using save instead of update_column. This means callbacks will not be skipped and all changes are saved instead of only the phones attribute.
From the comment I make out you're looking for a one liner, which isn't mutch different from the above:
person.tap { |person| person.phones[:home].sub!('001', '+1') }.save
You can use the before_validation callback on your model.
Like this:
class Phone < ApplicationRecord
validates :home, US_PHONE_REGEX
before_validation :normalize_number
private
def normalize_number
home.gsub!(/^001/, '+1')
end
end
Note: I haven't tested this code, it's meant to show an approach only.
If you're looking to normalize also an international number, evaluate if the use of a lib like phony wouldn't make more sense, or the rails lib https://github.com/joost/phony_rails based on it.
EDIT
since the comment clarify you only want to change the values of the hash in one like you can use Ruby's method transform_values!:
phones.transform_values!{|v| v.gsub(/^001/, '+1')}

Reliable String interpolation in Rails

What is the best way to ensure that a model exists before doing string interpolation? I have a variable user, and I need to see what the user's major is. A table in between named user_attributes has the user's info.
#{user.user_attribute.major.name}
The user may not have specified a major yet, in which case they wouldn't have a major model instance yet. So when I try and get the name of the major, I would get an "undefined method on nil class type" error. Any advice on how to safely do this?
You could avoid try and add a method to your model or decorator..
def major_name
user_attribute.major && user_attribute.major.name
end
OR
def major_name
user_attribute.major.name if user_attribute.major?
end
Check: https://codereview.stackexchange.com/questions/28610/handling-nil-trying-to-avoid-try
You can use try method:
# if model is present
{user.user_attribute.major.try(:name)} # => "<MAJOR_NAME>"
# if model is NOT present
{user.user_attribute.major.try(:name)} # => ""
You can read more about try.
You could use the lonely operator. It is like try, but slightly less functional (which doesn't matter in your case).
user.user_attribute.major&.name
This might well be the 'worst' answer to ruby puritans, but it works for me in some scenarios:
"#{user.user_attribute.major.name rescue nil}"

Setting virtual model attribute from controller create action in Rails 4 - attribute always nil

Some background: I have a Rails 4 model with a decimal column d. When being saved or viewed in the app, the value may need to be converted to or from an integer value, respectively, based on the current user's preference.
It seems most appropriate that the calculations should go in the model, and should have no knowledge of users.
After doing some searching, it seems to me the best way to achieve this is via 2 virtual attributes on the model: one for the current user's preferred format and one for the value, with any calculation applied.
So I have come up with something like this:
attr_accessor :format, :value_converted
def value_converted
if format.nil?
format = 'A'
end
if format == 'A'
Converter.new(d).to_i if d
else
d if d
end
end
def value_converted=(value)
if format.nil?
format = 'A'
end
if format == 'A'
self.d = Converter.new(value.to_i).to_f
else
self.d = value
end
Forms refer to the value_converted rather than the d, and this works fine for editing, updating and viewing. The problem is the #create action.
I believe the issue is happening because the setter is accessing format, and I can't seem to figure out a way to set format from #create before the setter is called, in other words when new creates the object.
I thought perhaps I could just pass the format along with the other parameters (as in this answer, so I tried merging it in, both within and after the standard strong parameters fare, but with no luck:
Both
#entry = current_user.entries.new(entry_params.merge(format: current_user.format))
and
params.require(:entry).permit(:value_converted, :other_param, ...).merge(format: current_user.format)
do not raise any errors, but are apparently ignored. However simply passing format to new in the console works fine.
So my question: Does anyone have a solution to this problem? Or perhaps a better/more appropriate way of going about it? It seems like it should be something simple.
Thanks for any help
For the moment, I got around this by simply setting the values of format and value_converted again in the controller before saving:
def create
#entry = current_user.entries.new(entry_params)
#entry.format = current_user.format
#entry.value_converted = entry_params[:value_converted]
if #entry.save
...
end
Though this is probably not the most elegant solution (I have no idea whether my implementation is thread-safe) it does seem to work.

Is the Active Record Base update method deprecated?

I'm trying to update many active records at the same time using the :update method and they don't seem to update fine.
#drop_ship_order_line_items = DropShipOrderLineItem.update(params[:drop_ship_order_line_items].keys, params[:drop_ship_order_line_items].values).reject { |dsoli| dsoli.errors.empty? }
params[:drop_ship_order_line_items] returns the following hash:
{"11"=>{"available"=>"1"}, "2"=>{"available"=>"1"}}
But the models don't seem to update correctly...anyone with insides?
AFAIK you can't update models like this on rails, you would have to do it like this:
params[:drop_ship_order_line_items].each do |key,value|
DropShipOrderLineItem.find( key ).update_attributes( value )
end
EDIT
There's probably an attr_protected call somewhere in your code, you should check which attributes are protected or not in there.
If you think you can safely ignore the protection on this specific call, you can use some sending do work out the magic (disclaimer: this is on your own, i'm just showing a possibility):
params[:drop_ship_order_line_items].each do |key,value|
ship = DropShipOrderLineItem.find( key )
value.each do |property,value|
ship.send( "#{property}=", value )
end
ship.save
end
This is going to overcome the attribute protection, but you should make sure this is a safe call and you're not going to burn yourself by doing this.

Rename ActiveResource properties

I am consuming JSON data from a third party API, doing a little bit of processing on that data and then sending the models to the client as JSON. The keys for the incoming data are not named very well. Some of them are acronyms, some just seem to be random characters. For example:
{
aikd: "some value"
lrdf: 1 // I guess this is the ID
}
I am creating a rails ActiveResource model to wrap this resource, but would not like to access these properties through model.lrdf as its not obvious what lrdf really is! Instead, I would like some way to alias these properties to another property that is named better. Something so that I can say model.id = 1 and have that automatically set lrdf to 1 or puts model.id and have that automatically return 1. Also, when I call model.to_json to send the model to the client, I dont want my javascript to have to understand these odd naming conventions.
I tried
alias id lrdf
but that gave me an error saying method lrdf did not exist.
The other option is to just wrap the properties:
def id
lrdf
end
This works, but when I call model.to_json, I see lrdf as the keys again.
Has anyone done anything like this before? What do you recommend?
Have you tried with some before_save magic? Maybe you could define attr_accessible :ldrf, and then, in your before_save filter, assign ldrf to your id field. Haven't tried it, but I think it should works.
attr_accessible :ldrf
before_save :map_attributes
protected
def map_attributes
{:ldrf=>:id}.each do |key, value|
self.send("#{value}=", self.send(key))
end
end
Let me know!
You could try creating a formatter module based on ActiveResource::Formats::JsonFormat and override decode(). If you had to update the data, you'd have to override encode() also. Look at your local gems/activeresource-N.N.N/lib/active_resource/formats/json_format.rb to see what the original json formatter does.
If your model's name is Model and your formatter is CleanupFormatter, just do Model.format = CleanupFormatter.
module CleanupFormatter
include ::ActiveResource::Formats::JsonFormat
extend self
# Set a constant for the mapping.
# I'm pretty sure these should be strings. If not, try symbols.
MAP = [['lrdf', 'id']]
def decode(json)
orig_hash = super
new_hash = {}
MAP.each {|old_name, new_name| new_hash[new_name] = orig_hash.delete(old_name) }
# Comment the next line if you don't want to carry over fields missing from MAP
new_hash.merge!(orig_hash)
new_hash
end
end
This doesn't involve aliasing as you asked, but I think it helps to isolate the gibberish names from your model, which would never have to know those original names existed. And "to_json" will display the readable names.

Resources